pax_global_header00006660000000000000000000000064134730565300014520gustar00rootroot0000000000000052 comment=215e0064c99f8598173493cf741971ba7eda720a vcr-5.0.0/000077500000000000000000000000001347305653000123145ustar00rootroot00000000000000vcr-5.0.0/.github/000077500000000000000000000000001347305653000136545ustar00rootroot00000000000000vcr-5.0.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001347305653000160375ustar00rootroot00000000000000vcr-5.0.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000007431347305653000205350ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- {{Replace with description of the problem and code or logs to help explain your problem}} {{Include VCR configuration and appropriate vcr cassettes}} Ruby {{Replace with version}} Gem {{Replace with version}} HTTP {{Replace with name and version}} Mock {{Replace with name and version}} Rails {{Replace with version if applicable}} Rspec {{Replace with version if applicable}} vcr-5.0.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011231347305653000215610ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. vcr-5.0.0/.gitignore000066400000000000000000000012371347305653000143070ustar00rootroot00000000000000# See http://help.github.com/ignore-files/ for more about ignoring files. # # If you find yourself ignoring temporary files generated by your text editor # or operating system, you probably want to add a global ignore instead: # git config --global core.excludesfile ~/.gitignore # Ignore all of the generated gem stuff /pkg /*.gem # Ignore bundler config /bin /.bundle /Gemfile*.lock # Ignore autoset environment variables /.ruby-env # Ignore all bundler caching /vendor/cache /vendor/bundle # Ignore all tempfiles /tmp # Ignore documentation cache and generated files /.yardoc/ /_yardoc/ /doc/ /rdoc/ # Ignores that should be in the global gitignore /coverage vcr-5.0.0/.rspec000066400000000000000000000000101347305653000134200ustar00rootroot00000000000000--color vcr-5.0.0/.travis.yml000066400000000000000000000002341347305653000144240ustar00rootroot00000000000000sudo: false language: ruby script: script/ci.sh before_install: - gem update --system - gem install bundler cache: bundler rvm: - 2.4 - 2.5 - 2.6 vcr-5.0.0/.yardopts000066400000000000000000000001671347305653000141660ustar00rootroot00000000000000--no-private --exclude features --markup markdown --hide-void-return - CHANGELOG.md CONTRIBUTING.md LICENSE Upgrade.md vcr-5.0.0/Appraisals000066400000000000000000000002221347305653000143320ustar00rootroot00000000000000appraise "typhoeus-old" do gem 'typhoeus', '~> 0.4.2' gem 'webmock', '1.8.11' end appraise "faraday-old" do gem 'faraday', '~> 0.8.8' end vcr-5.0.0/CHANGELOG.md000066400000000000000000001335611347305653000141360ustar00rootroot00000000000000Changelog ========= ## 4.0.0 [Full Changelog](https://github.com/vcr/vcr/compare/v3.0.3...v4.0.0) - [Feature] Output non-matched headers on error - [Fix] Allow non-latin charaters in cassette filenames - [Breaking] Remove deprecated VCR::RSpec::Macros - [Breaking] Remove support for ruby 1.9.3 - [Breaking] Remove support for Fakeweb ## 3.0.3 [Full Changelog](https://github.com/vcr/vcr/compare/v3.0.2...v3.0.3) Bug Fixes: * rebuild Gem with ruby 2.2 to avoid rubygems issue https://github.com/rubygems/rubygems/issues/1448 ## 3.0.2 [Full Changelog](https://github.com/vcr/vcr/compare/v3.0.1...v3.0.2) Bug Fixes: * Support WebMock 2.0.0 by calling `WebMock.enable!` when the library hook is enabled. (Tony Miller) * fix webmock thread safety for with_global_hook_disabled (Jan Berdajs) ## 3.0.1 [Full Changelog](https://github.com/vcr/vcr/compare/v3.0.0...v3.0.1) * [Adding] Use the specs scope as the cassette name when the description is empty (Philipp Tessenow) * [Fixing] make ignore_cassettes a boolean again to prevent nil.dup crashes (Harald Sitter) * [Fixing] Fix issue #517 Cucumber scenario outlines use incorrect cassette (Jan Berdajs) * [Fixing] fix(persisters): use binary mode in read/write (KARASZI István) ## 3.0.0 [Full Changelog](https://github.com/vcr/vcr/compare/v2.9.3...v3.0.0) * [Breaking] test support for 1.8.7, 1.9.2, 2.0.0, 2.1.0, ree, jruby 1.8mode, rbx 1.8mode * [Breaking] the possible return value `VCR.configuration`, it now might return `nil` * [Breaking] the possible return value `VCR.cassette_serializers`, it now might return `nil` * [Breaking] the possible return value `VCR.cassette_persisters`, it now might return `nil` * [Breaking] the possible return value `VCR.library_hooks`, it now might return `nil` * [Breaking] the possible return value `VCR.request_ignorer`, it now might return `nil` * [Breaking] the possible return value `VCR.request_matchers`, it now might return `nil` * [Breaking] the threadness of VCR, by using `Mutex` * [Adding] a new `:compressed` value for serializers that stores in gzipped files * [Adding] support for farady's `RackBuilder` if it exists * [Changing] the cucumber scenario naming mechanism ## 2.9.3 (September 7, 2014) [Full Changelog](http://github.com/vcr/vcr/compare/v2.9.2...v2.9.3) Bug Fixes: * Fix `VCR::Cassette#serializable_hash` so that it does not allow `before_record` hooks to apply mutations to existing HTTPInteraction instances. (Myron Marston) ## 2.9.2 (May 27, 2014) [Full Changelog](http://github.com/vcr/vcr/compare/v2.9.1...v2.9.2) Bug Fixes: * Fix RSpec metadata integration once more -- we changed it a bit more in response to user feedback. (Myron Marston) ## 2.9.1 (May 23, 2014) [Full Changelog](http://github.com/vcr/vcr/compare/v2.9.0...v2.9.1) Bug Fixes: * Fix RSpec metadata integration to not trigger deprecation warnings with RSpec 3.0.0.rc1+. (Janko Marohnić) ## 2.9.0 (March 27, 2014) [Full Changelog](http://github.com/vcr/vcr/compare/v2.8.0...v2.9.0) Enhancements: * Update version checking to only assert that a given library is >= a minimum version. (Ryan Foster) * Explicitly support the latest Excon release (0.32). (Ryan Foster) * Explicitly support the latest Excon release (0.31). (Michiel de Mare) * Explicitly support the latest Webmock releases (1.16, 1.17). (Ryan Foster, Lawson Kurtz) ## 2.8.0 (November 23, 2013) [Full Changelog](http://github.com/vcr/vcr/compare/v2.7.0...v2.8.0) Enhancements: * Explicitly support the latest Excon release (0.29). (Myron Marston) * Add `:body_as_json` request matcher. (Mike Dalton) * Include the body in the `UnhandledHTTPRequestError` message when matching on `:body` to help identify the request. (Chris Gunther) Bug Fixes: * Fix Excon adapter so that it properly records responses even when a middleware raises an error (such as via the `:expects` Excon option). Previously, the order `response_call` was invoked on Excon middleware caused VCR's recording ot be skipped when an error was raised by another middleware. To fix this, we have split up VCR Excon middleware into two middlewares that we can insert into the stack at the appropriate spots. Note that to get this to work, Excon < 0.25.2 is no longer supported. (Myron Marston) * Fix Excon adapter so that we pass it a dup of the body string rather than the body string itself, since Excon has code paths that will mutate the stubbed response string we give it, wreaking confusing havoc. (Myron Marston) * Fix rspec metadata implementation so that it does not emit warnings on RSpec 2.99. (Herman Verschooten) ## 2.7.0 (October 31, 2013) [Full Changelog](http://github.com/vcr/vcr/compare/v2.6.0...v2.7.0) Enhancements: * Explicitly support the latest WebMock releases (1.14 and 1.15). (Eduardo Maia, Johannes Würbach) * Explicitly support the latest Excon releases (0.27 and 0.28). (Myron Marston) * Add support for Excon unix sockets by leveraging its new `::Excon::Utils.request_uri` method. (Todd Lunter) * Reword the "it may not work with this version" warning message so the intent is more clear (Myron Marston). * Support post/put bodies being specified as a hash when using Typhoeus by leveraging it's new `encoded_body` API. (Myron Marston, Hans Hasselberg) Bug Fixes: * Fix detection of encoding errors for MultiJson 1.8.1+. (Myron Marston). * Fix file name sanitization to better handle paths that have a dot in them (Rob Hanlon, Myron Marston). * Fix Faraday middleware so that it works properly when another adapter is exclusively enabled (Myron Marston). ## 2.6.0 (September 25, 2013) [Full Changelog](http://github.com/vcr/vcr/compare/v2.5.0...v2.6.0) Enhancements: * Add `VCR::Cassette#originally_recorded_at` for use when freezing time based on when the cassette was recorded. (Myron Marston) * Improve the `:allow_unused_http_interactions => false` option so that it does not raise an error when there are unused interactions due to the test failing on its own; otherwise, it could raise an error and silence the original test failure. (Myron Marston) * Improve perf when no logger is used by having it short-circuit and not bother formatting a logger message that won't be printed, anyway (Luan Santos and Matt Parker). Bug Fixes: * Fix confusing errors that could result when using the YAML serializer if the client code added some state (e.g. via an extension module) onto a request or response body. (Myron Marston) * Ensure response body is always recorded when hooking into `:excon`, even when using a `:response_block` and an unexpected status is returned. Excon doesn't invoke the `:response_block` in this case, requiring special handling. (James Bence) * Explicitly support the latest WebMock (1.13). (Ron Smith) * Explicitly support the latest Excon (0.26). (Myron Marston) * Fix detection of encoding errors to handle `ArgumentError` that is raised by recent versions of `MultiJson`. (Myron Marston) * Fix Excon adapter so that it allows VCR to play nicely with manual Excon stubs (using Excon's `Excon.stub` API). (Myron Marston) * Fix Typhoeus adapter so that it sets `effective_url` properly when the `:followlocation` option is used and a redirect is followed. (Myron Marston) ## 2.5.0 (May 18, 2013) [Full Changelog](http://github.com/vcr/vcr/compare/v2.4.0...v2.5.0) Bug Fixes: * Fix `around_http_request` so that it does not raise confusing errors when requests are made in multiple threads. * Fix `configure_rspec_metadata!` so that you can safely call it more than once Enhancements: * Relax WebMock version checker to allow WebMock 1.10 and 1.11 without issuing warnings (Johannes Würbach and Myron Marston). * Update Excon integration to take advantage of new Excon middleware architecture. This is a more robust way to hook into Excon and will be less prone to breakage due to internal Excon changes (Myron Marston). Deprecations: * Deprecate support for Typhoeus < 0.5. It will be removed in VCR 3.0 (Sheel Choksi). ## 2.4.0 (January 4, 2013) [Full Changelog](http://github.com/vcr/vcr/compare/v2.3.0...v2.4.0) Enhancements: * Add `:query` request matcher. The new `query_parser` config option can bet set to change how the query is parsed. Thanks to [Nathaniel Bibler](https://github.com/nbibler) for implementing this. Bug Fixes: * Fix previously recorded requests not matching when using the URIWithoutParams builtin matcher. In the case where the original request was recorded without parameters and subsequent requests filter out all parameters, the subsequent requests were failing to match the originally recorded request. Thanks to [Dmitry Jemerov](https://github.com/yole) for reporting the issue and [Nathaniel Bibler](https://github.com/nbibler) for implementing the fix. * Set `effective_url` on Typhoeus response when playing back. Thanks to [Shay Frendt](https://github.com/shayfrendt) and [Ryan Castillo](https://github.com/rmcastil) for providing the fix and corresponding test. Deprecations: * Deprecate the `use_vcr_cassette` macro for RSpec. It has confusing semantics (e.g. calling it multiple times in the same example group can cause problems and it uses the same cassette for all examples in a group even though they may make different HTTP requests) and VCR's integration with RSpec metadata works much better. Thanks to [Austen Ito](https://github.com/austenito) for implementing this. * Deprecate integration with FakeWeb. FakeWeb appears to be no longer maintained (0 commits in 2012 and it has pull requests that are 2 years old) and WebMock is a far better option. Thanks to [Steve Faulkner](https://github.com/southpolesteve) for implementing this. ## 2.3.0 (October 29, 2012) [Full Changelog](http://github.com/vcr/vcr/compare/v2.2.5...v2.3.0) Enhancements: * Add `uri_parser` configuration option. It defaults to `URI` but can be set to something like `Addressable::URI` to handle non-standard URIs that stdlib `URI` doesn't handle. Thanks to [Ryan Burrows](https://github.com/rhburrows) for contributing this feature. * Add support for Typhoeus 0.5. Thanks to [Hans Hasselberg](https://github.com/i0rek) for making the needed changes. Bug Fixes: * Fix `:use_scenario_name` cucumber tag option so that it only uses the first line of the scenario name. Scenarios can include a long preamble that Cucumber includes as part of the scenario name. Thanks to [Pascal Van Hecke](https://github.com/pascalvanhecke) for providing this fix. ## 2.2.5 (September 7, 2012) [Full Changelog](http://github.com/vcr/vcr/compare/v2.2.4...v2.2.5) Enhancements: * Include note about `debug_logger` option in error message for unhandled HTTP requests. Thanks to [Jacob Green](https://github.com/Jacobkg) for implementing this. Bug Fixes: * Fix another edge case bug on the excon adapter that was causing it to mis-record in certain situations that used Excon's :expects option. * Fix the `:use_scenario_name` cucumber tags option to work properly with scenario outlines. Thanks to [Joe Nelson](https://github.com/begriffs) and [Stephen Anderson](https://github.com/bendycode) for the initial bug fixes and [Jacob Green](https://github.com/Jacobkg) for some further improvements. ## 2.2.4 (July 19, 2012) [Full Changelog](http://github.com/vcr/vcr/compare/v2.2.3...v2.2.4) Bug Fixes: * Fix excon so real requests are made with a connection constructed with same args as the original connection. ## 2.2.3 (July 9, 2012) [Full Changelog](http://github.com/vcr/vcr/compare/v2.2.2...v2.2.3) Bug Fixes: * Fix FakeWeb library hook so that it properly handles the case where multiple requests are made using the same Net::HTTP request object. Previously, a `NoMethodError` was raised. Thanks to [Jacob Green](https://github.com/Jacobkg) for helping to troubleshoot this bug! ## 2.2.2 (June 15, 2012) [Full Changelog](http://github.com/vcr/vcr/compare/v2.2.1...v2.2.2) Bug Fixes: * Fix `VCR.eject_cassette` so that it always pops a cassette off the cassette stack even if an error occurs while ejecting the cassette. This is important to keep things consistent, so that a cassette for one test doesn't remain in place for another test. ## 2.2.1 (June 13, 2012) [Full Changelog](http://github.com/vcr/vcr/compare/v2.2.0...v2.2.1) Bug Fixes: * Fix matcher generated by `VCR.request_matchers.uri_without_params` so that it handles URIs w/o query params properly. Previously, it would allow any two URIs w/o query params to match, even if the hosts or paths differed. ## 2.2.0 (May 31, 2012) [Full Changelog](http://github.com/vcr/vcr/compare/v2.1.1...v2.2.0) Enhancements: * Add new `:persist_with` cassette option. It allows you to provide a customized persistence implementation so you can persist it to something other than disk (i.e. a key-value store or a database). Thanks to [Chris Le](https://github.com/chrisle) for the idea and help with the implementation. * Allow requests to be stubbed by external libraries (e.g. WebMock, FakeWeb or Typhoeus) without needing to turn VCR off. * Add new `:allow_unused_http_interactions` cassette option. When set to false, an error will be raised when a cassette is ejected and there are remaining unused HTTP interactions. Thanks to [Mattias Putman](https://github.com/challengee) for the idea and initial implementation. Bug Fixes: * Fix `after_http_request` to handle symbol request predicate filters (e.g. `:ignored?`, `:stubbed?`, `:recordable?`, `:unhandled?`, `:real?`) properly. Previously using one of these would raise an ArgumentError. Thanks to [playupchris](https://github.com/playupchris) for reporting the bug and providing a fix. * Fix FakeWeb hook so that it no longer breaks `FakeWeb.allow_net_connect?` with arguments. Thanks to [Ingemar](https://github.com/ingemar) for reporting the bug and providing a fix. * Fix WebMock hook so that it no longer breaks `WebMock.net_connect_allowed?` with arguments. Thanks to [Gordon Wilson](https://github.com/gordoncww) for reporting the bug and providing a fix. * Print a warning when VCR is used with a poorly behaved Faraday connection stack that has a middleware after the HTTP adapter. VCR may work improperly in this case. * Raise an error if a response object is recorded with a non-string body. This fails early and indicates the problem rather than failing later with a strange error. * Fix `filter_sensitive_data`/`define_cassette_placeholder` so that they handle non-strings gracefully (e.g. the port number as a Fixnum). * Gracefully handle Faraday connection stacks that do not explicitly specify an HTTP adapter. Thanks to [Patrick Roby](https://github.com/proby) for reporting the bug. * Work around a bug in WebMock's em-http-request adapter that prevented VCR from working when using the `:redirects` option with em-http-request. This change is just a work around. It fixes the main problem, but some features (such as the http request hooks) may not work properly for this case. The bug will ultimately need to be [fixed in WebMock](https://github.com/bblimke/webmock/pull/185). Thanks to [Mark Abramov](https://github.com/markiz) for reporting the bug and providing a great example test case. * Fix bug in handling of Faraday requests with multipart uploads. Thanks to [Tyler Hunt](https://github.com/tylerhunt) for reporting and fixing the bug. ## 2.1.1 (April 24, 2012) [Full Changelog](http://github.com/vcr/vcr/compare/v2.1.0...v2.1.1) * Fix `:use_scenario_name` cucumber tag option so that it works properly with multiple scenarios. Thanks to [Brent Snook](https://github.com/brentsnook) for reporting this bug. * Fix `:use_scenario_name` cucumber tag option so that it only uses the first line of the scenario feature name. Cucumber includes all of the pre-amble text in the feature name but that can create a ridiculously long cassette name. Thanks to [Brent Snook](https://github.com/brentsnook) for reporting this bug. ## 2.1.0 (April 19, 2012) [Full Changelog](http://github.com/vcr/vcr/compare/v2.0.1...v.2.1.0) * Add new `:use_scenario_name` option to the cucumber tags API. This allows you to use a generic tag (such as `@vcr`) and have the cassettes named based on the feature and scenario rather than based on the tag. Thanks to [Omer Rauchwerger](https://github.com/rauchy) for the implementation and [Chad Jolly](https://github.com/cjolly) for the initial idea and feedback. * Add new `:decode_compressed_response` cassette option. When set to true, VCR will decompress a gzipped or deflated response before recording the cassette, in order to make it more human readable. Thanks to [Mislav Marohnić](https://github.com/mislav) for the idea and implementation. ## 2.0.1 (March 30, 2012) [Full Changelog](http://github.com/vcr/vcr/compare/v2.0.0...v2.0.1) * Fix encoding logic to not attempt to encode the request or response body on deserialization if there is no encoding specified. This should allow cassettes recorded on 1.8 to work on 1.9. Thanks to [Kevin Menard](https://github.com/nirvdrum) for reporting the bug. * Fix Excon adapter to fix a bug with Excon 0.11 and greater. When you passed a block to an excon request, the response body would not be recorded. * Fix Faraday middleware so that it plays back parallel requests properly. Thanks to [Dave Weiser](https://github.com/davidann) for reporting this bug. ## 2.0.0 (March 2, 2012) [Full Changelog](http://github.com/vcr/vcr/compare/v2.0.0.rc2...v2.0.0) * Add some additional logged events for the `debug_logger`. * Don't worry about stripping the standard port from the request URI on playback. The standard port only needs to be stripped during recording; for playback, it will have already been stripped. This allows people to use the `filter_sensitive_data` option in a way that changes the URI; before this change, doing so could result in `URI::InvalidURIError`. Thanks to [Patrick Schmitz](https://github.com/bullfight) and [Dan Thompson](https://github.com/danthompson) for reporting the issue and helping diagnose it. * Relax Excon dependency to include newly released 0.10. * Relax Faraday dependency to include 0.8. * Fix Faraday library hook so that it always does the version checking. ## 2.0.0 RC 2 (February 23, 2012) [Full Changelog](http://github.com/vcr/vcr/compare/v2.0.0.rc1...v2.0.0.rc2) ## New Features * Add YARD documentation for the public API. Thanks to [Ben Oakes](https://github.com/benjaminoakes) for help with setting this up. * Fix `around_http_request` hook so that `request.proceed` returns the response. * Resolve `cassette_library_dir` to an absolute path. Thanks to [Nate Clark](https://github.com/heythisisnate) for the suggestion. * Add to the `VCR::Request` API in `before_http_request` and `after_http_request` hooks so the request has query methods like `#real?`, `#recordable?`, `#ignored?`, etc. Thanks to [Nate Clark](https://github.com/heythisisnate) for the idea. * Allow filters (objects that respond to `#to_proc`) to be passed to `before_http_request` and `after_http_request`. This allows an API like `before_http_request(:real?)` or `after_http_request(lambda { |req| req.uri =~ /amazon/ })`. * Add `debug_logger` config option. This can be used to troubleshoot what VCR is doing. * Update WebMock to version (1.8.0) that supports Excon stubbing. * Store the encoding with the request & response bodies in the serialized cassette. * Add new `preserve_exact_body_bytes` option that base64 encodes the request or response body in order to preserve the bytes exactly. Thanks to [Jeff Pollard](https://github.com/Fluxx) for help designing this feature and for code reviewing it. * Update to and require latest Excon (0.9.6). ## Bug Fixes * Fix rspec metadata integration to allow the cassette name to be set at the example group level and apply to multiple examples. Thanks to [Paul Russell](https://github.com/pauljamesrussell) for reporting the bug. * Add missing `require 'vcr/version'` to the cassette migrator task. If you tried the migration rake task with 2.0.0.rc1 and got a `NoMethodError`, it should be fixed now. * Update Excon dependency to 0.9.5; 0.9.5 includes an important bug fix needed by VCR. * Ensure the excon retry limit is honored properly. * Ensure that the correct error class is raised by excon when stubbing an unexpected status. * Fix FakeWeb library hook so that it records the request body when using `Net::HTTP.post_form`. Thanks to [Retistic](https://github.com/Retistic) for reporting the bug. ## 2.0.0 RC 1 (December 8, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v2.0.0.beta2...v2.0.0.rc1) * Add Faraday hook that automatically inserts the VCR middleware so that you can use VCR with Faraday without needing to insert the middleware yourself. Use `VCR.configure { |c| c.hook_into :faraday }`. * Add `ignore_request` config option. Pass it a block that returns true if the given request should be ignored. * Improve the unhandled HTTP request error message so that it lists different options for how to get VCR to handle it. * Add {before,after,around}_http_request hooks. * Updated WebMock integration and bumped up required version to 1.7.8. * Test against latest Excon (0.7.9) and confirm that VCR works fine with it. * Add define_cassette_placeholder as an alias for filter_sensitive_data. * Fix Faraday middleware so that it works properly when you use parallel requests. * Integrate VCR with RSpec metadata. Thanks to [Ryan Bates](https://github.com/ryanb) for the great idea. ## 2.0.0 Beta 2 (November 6, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v2.0.0.beta1...v2.0.0.beta2) * Update to (and require) Typhoeus 0.3.2. * Fix a bug with `VCR.request_matchers.uri_without_param(:some_param)` so that it properly handles URIs that have no parameters. Thanks to [Sathya Sekaran](https://github.com/sfsekaran) for this fix. * The cassette format has changed significantly: * The HTTPInteractions are no longer normalized in a lossy fashion. VCR 1.x converted all HTTP header keys to lowercase. VCR 2.0 no longer does this because it is impossible to know what the original casing was (i.e. given `etag`, was it originally `etag`, `ETag` or `Etag`?). Also, some HTTP libraries add particular request headers to every request, and these used to be ignored. The aren't anymore. * The ruby struct objects are not directly serialized anymore. Instead, only primitives (hashes, arrays, strings, integers) are serialized. This allows swappable serializers and will allow other tools to read and use a VCR cassette. * Add new serializer API. VCR ships with YAML, Syck, Psych and JSON serializers, and it is very simple to implement your own. The serializer can be configured on a per-cassette basis. * New `vcr:migrate_cassettes DIR=path/to/cassettes` rake task assists with upgrading from VCR 1.x to 2.0. * Cassettes now contain a `recorded_with` attribute. This should allow the cassette structure to be updated more easily in the future as the version number provides a means for easily migrating cassettes. * Add `recorded_at` to data serialized with an HTTPInteraction. This allows the `:re_record_interval` cassette option to work more accurately and no longer rely on the file modification time. Note that VCR 1.x cassettes cannot be used with VCR 2.0. See the upgrade notes for more info. ## 2.0.0 Beta 1 (October 8, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.11.3...v2.0.0.beta1) ### Changed * Previously, the last matching response in a cassette would repeatedly playback if the same request kept being made. This is no longer the case. * The Faraday middleware has been rewritten. * You no longer need to configure `stub_with :faraday` to use it. * It has been updated to work in parallel mode. * It no longer accepts a block and uses that to determine the cassette. Instead, use `VCR.use_cassette` just like you would with FakeWeb or WebMock. ### Added * Allow any callable (an object that responds to #call, such as a lambda) to be used as a request matcher. Thanks to [Avdi Grimm](https://github.com/avdi) for the idea. * Add ability to register custom request matchers. * Add `VCR.request_matchers.uri_without_param(:some_param)` to generate a request matcher that matches on URI but ignores the named parameter. * New `:allow_playback_repeats` cassette option preserves the old playback repeat behavior. Thanks to [Avdi Grimm](https://github.com/avdi) for the idea. * New `:exclusive` cassette option allows a cassette to be exclusively used rather than keeping the existing one active as a fallback. Thanks to [Avdi Grimm](https://github.com/avdi) for the idea. ### Removed * Removed support for Ruby 1.8.6 and 1.9.1. * Removed lots of old deprecated APIs. * Removed support for manually changing the URI in a cassette to a regex. ### Deprecated * Deprecated `VCR.config` in favor of `VCR.configure`. * Deprecated `VCR::Config` singleton module in favor of `VCR::Configuration` class. The current configuration instance can be accessed via `VCR.configuration`. * Deprecated `stub_with` in favor of `hook_into`. The stubbing adapters have been completely rewritten and are no longer an implementation of the adapter design pattern. Instead they simply use the named library to globally hook into every HTTP request. ## 1.11.3 (August 31, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.11.2...v1.11.3) * Fix cassette serialization so that it does not include extra `ignored` instance variable. ## 1.11.2 (August 28, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.11.1...v1.11.2) * Updated rake, cucumber and aruba dev dependencies to latest releases. * Fix all warnings originating from VCR. VCR is now warning-free! ## 1.11.1 (August 18, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.11.0...v1.11.1) * Yanked 1.11.0 and rebuilt gem on 1.8.7 to deal with syck/psych incompatibilties in gemspec. ## 1.11.0 (August 18, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.10.3...v1.11.0) * Updates to work with WebMock 1.7.0. ## 1.10.3 (July 21, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.10.2...v1.10.3) * Fix `:update_content_length_header` option so no error is raised if a response body is nil. Bug reported by [jg](https://github.com/jg). ## 1.10.2 (July 16, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.10.1...v1.10.2) * Yanked 1.10.1 and rebuilt gem on 1.8.7 to deal with syck/psych incompatibilties in gemspec. ## 1.10.1 (July 16, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.10.0...v1.10.1) * Fix typo in error message. Fix provided by [Bradley](https://github.com/bradleyisotope). * Fix excon adapter to properly handle queries specified as a hash. * Fix excon adapter to stub a response with a hash as excon expects. Fix provided by [Wesley Beary](https://github.com/geemus). * Fix excon adapter so that it records a response even when excon raises an error due to an unexpected response. ## 1.10.0 (May 18, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.9.0...v1.10.0) * Fix header normalization so that it properly handles nested arrays and non-string values. * Add cucumber scenario documenting how VCR sanitizes cassette names to "normal" file names (i.e. only alphanumerics, no spaces). * Add `:ignore_cassettes` option to `VCR.turn_off!`. This causes cassette insertions to be ignored rather than to trigger an error. Patch provided by [Justin Smestad](https://github.com/jsmestad). * Fix rack middleware to make it threadsafe. * Update to latest RSpec (rspec 2.6). ## 1.9.0 (April 14, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.8.0...v1.9.0) * Add support for [Excon](https://github.com/geemus/excon). ## 1.8.0 (March 31, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.7.2...v1.8.0) * Updated Faraday middleware to work with newly released Faraday 0.6.0. ## 1.7.2 (March 26, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.7.1...v1.7.2) * Fixed Typhoeus adapter so headers are returned in the same form during playback as they would be without VCR. Bug reported by [Avdi Grimm](https://github.com/avdi). * Fixed Faraday adapter so it treats response headers in the same way Faraday itself does (i.e. with lowercase keys). ## 1.7.1 (March 19, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.7.0...v1.7.1) * Fix Faraday adapter so that it properly normalizes query parameters in the same way that Faraday itself does. ## 1.7.0 (March 1, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.6.0...v1.7.0) * Use Psych for YAML serialization/deserialization when it is available. Syck, Ruby's old YAML engine, will remove whitespace from some strings. Bug reported by [Robert Poor](https://github.com/rdpoor). * Add new `:update_content_length_header` cassette option. The option will ensure the `content-length` header value matches the actual response body length. * Add new `:once` record mode. It operates like `:new_episodes` except when the cassette file already exists, in which case it causes new requests to raise an error. Feature suggested by [Jamie Cobbett](https://github.com/jamiecobbett). * Made `:once` the default record mode. * Add new `filter_sensitive_data` configuration option. Feature suggested by [Nathaniel Bibler](https://github.com/nbibler). * Commit to [Semantic Versioning](http://semver.org/). The cucumber features document the public API for the purposes of semver. * Add support for CI builds using [travis-ci](http://travis-ci.org/myronmarston/vcr). * Add support for running tests through `gem test vcr`. Visit [test.rubygems.org](http://test.rubygems.org/gems/vcr) to see the results. * Fix cucumber support to use separate `Before` & `After` hooks rather than a single `Around` hook because of a bug in cucumber that prevents background steps from running within the `Around` hook. ## 1.6.0 (February 3, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.5.1...v1.6.0) * Add new `ignore_hosts` configuration option that allows you to ignore any host (not just localhost aliases, as the `ignore_localhost` option works). Feature suggested by [Claudio Poli](https://github.com/masterkain). * Upgraded to the latest Typhoeus (0.2.1). * General code clean up and refactoring. ## 1.5.1 (January 12, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.5.0...v1.5.1) * Fix response and request serialization so that the headers are raw strings. This fixes intermittent YAML seg faults for paperclip uploads to S3. Bug reported by [Rob Slifka](https://github.com/rslifka). ## 1.5.0 (January 12, 2011) [Full Changelog](http://github.com/vcr/vcr/compare/v1.4.0...v1.5.0) * Fix VCR::Cassette so it does not raise an error when a cassette file is empty. Bug reported and fixed by [Karl Baum](https://github.com/kbaum). * Lots of code cleanup. * Fix the stubbing adapters so that they use the cassette instance rather than the cassette name to create and restore checkpoints. * Raise an appropriate error when a nested cassette is inserted with the same name as a cassette that is already in the stack (VCR's design doesn't allow this and you would get weird errors later on). * Raise an appropriate error when restoring a stubs checkpoint if the checkpoint cannot be found. * Add `before_record` and `before_playback` hooks. Idea and initial implementation by [Oliver Searle-Barnes](https://github.com/opsb); futher suggestions, testing and feedback by [Nathaniel Bibler](https://github.com/nbibler). ## 1.4.0 (December 3, 2010) [Full Changelog](http://github.com/vcr/vcr/compare/v1.3.3...v1.4.0) * Added support for making HTTP requests without a cassette (i.e. if you don't want to use VCR for all of your test suite). There are a few ways to enable this: * In your `VCR.config` block, set `allow_http_connections_when_no_cassette` to true to allow HTTP requests without a cassette. * You can temporarily turn off VCR using `VCR.turned_off { ... }`. * You can toggle VCR off and on with `VCR.turn_off!` and `VCR.turn_on!`. * Fixed bug with `ignore_localhost` config option. Previously, an error would be raised if it was set before the `stub_with` option. * Added VCR::Middleware::Rack (see features/middleware/rack.feature for usage). * Added support for Faraday (see features/middleware/faraday.feature for usage). ## 1.3.3 (November 21, 2010) [Full Changelog](http://github.com/vcr/vcr/compare/v1.3.2...v1.3.3) * In specs, hit a local sinatra server rather than example.com. This makes the specs faster and removes an external dependency. The specs can pass without being online! * Raise an explicit error when the http stubbing library is not configured (rather than letting the user get a confusing error later). * Test against the latest WebMock release (1.6.1) (no changes required). * Fix a few cucumber scenarios so they pass on rubinius and jruby. ## 1.3.2 (November 16, 2010) [Full Changelog](http://github.com/vcr/vcr/compare/v1.3.1...v1.3.2) * Fix serialized structs so that they are normalized andthey will be the same regardless of which HTTP library made the request. * Status "OK " => "OK" * Body '' => nil * Headers {} => nil * Remove extraneous headers added by the HTTP lib (i.e. Typhoeus user agent) * Rewrite cucumber features in a more documentation-oriented style. ## 1.3.1 (November 11, 2010) [Full Changelog](http://github.com/vcr/vcr/compare/v1.3.0...v1.3.1) * Update WebMock adapter to work with (and require) newly released WebMock 1.6.0. ## 1.3.0 (November 11, 2010) [Full Changelog](http://github.com/vcr/vcr/compare/v1.2.0...v1.3.0) * Moved documentation from README to [Wiki](http://github.com/vcr/vcr/wiki). * Refactoring and code cleanup. * Fix InternetConnection.available? so that it memoizes correctly when a connection is not available. * Fix WebMock version checking to allow newly released 1.5.0 to be used without a warning. * Add support for [Typhoeus](https://github.com/pauldix/typhoeus). Thanks to [David Balatero](https://github.com/dbalatero) for making the necessary changes in Typhoeus to support VCR. * Remove FakeWeb/WebMock inference logic. You _must_ configure the http stubbing library explicitly now. ## 1.2.0 (October 13, 2010) [Full Changelog](http://github.com/vcr/vcr/compare/v1.1.2...v1.2.0) * Improved the `:all` record mode so that it keeps previously recorded interactions that do not match the new recorded interactions. Previously, all of the previously recorded interactions were deleted. * Added `:re_record_interval` cassette option. This option causes a cassette to be re-recorded when the existing file is older than the specified interval. * Improved RSpec support. Added #use_vcr_cassette RSpec macro method that sets up a cassette for an RSpec example group. * Fixed VCR/Net::HTTP/WebMock integration so that VCR no longer loads its Net::HTTP monkey patch when WebMock is used, and relies upon WebMock's after_request callback to record Net::HTTP instead. This fixes [a bug](http://github.com/vcr/vcr/issues/14) when using WebMock and Open URI. * Consider 0.0.0.0 to be a localhost alias (previously only "localhost" and 127.0.0.1 were considered). * Added spec and feature coverage for Curb integration. Works out of the box with no changes required to VCR due to [Pete Higgins'](http://github.com/phiggins) great work to add Curb support to WebMock. * Got specs and features to pass on rubinius. * Changed WebMock version requirement to 1.4.0. ## 1.1.2 (September 9, 2010) [Full Changelog](http://github.com/vcr/vcr/compare/v1.1.1...v1.1.2) * Fixed a minor bug with the WebMock integration: WebMock extends each `Net::HTTPResponse` with an extension module after reading the body, and VCR was doing the same thing, leading to some slight deviance from standard Net::HTTP behavior. The fix prevents VCR from adding the same extension to a `Net::HTTPResponse` that has already been extende by WebMock. * Fixed a minor bug in the `VCR::Net::HTTPResponse` module so that it correctly handles nil bodies (such as for a HEAD request). * Refactored `VCR::Net::HTTPResponse` module so it is implemented in a much simpler manner. * Updated specs and features so they pass against the latest WebMock release (1.3.5). * Minor documentation updates. ## 1.1.1 (August 26, 2010) [Full Changelog](http://github.com/vcr/vcr/compare/v1.1.0...v1.1.1) * Updated to use and require FakeWeb 1.3.0. It includes a fix for a bug related to multiple values for the same response header. * Use new `FakeWeb::Utility.request_uri_as_string` method rather than our own logic to construct a request uri. * Use new `FakeWeb.allow_net_connect = /url regex/` feature to power the `ignore_localhost` VCR option rather then toggling `FakeWeb.allow_net_connect` in our Net::HTTP extension. * Optimized `VCR.http_stubbing_adapter.stub_requests` a bit. * Changed the http stubbing adapters to be modules rather than classes. They should never be instantiated and don't really hold state, so a module is more appropriate. * Warn when FakeWeb or WebMock are a minor or major version number ahead of the required version, as the new version isn't known to work with VCR. ## 1.1.0 (August 22, 2010) [Full Changelog](http://github.com/vcr/vcr/compare/v1.0.3...v1.1.0) * Added `:match_requests_on` cassette option, which determines how VCR matches requests. * Removed VCR::TaskRunner and the corresponding rake task definition. The rake task migrated cassettes from the 0.3.1 format to the 0.4+ format. If you are still on 0.3.1 or earlier, I recommend you upgrade to 0.4.1 first, migrate your cassettes and deal with migration warnings, then upgrade to the current release. * Added some code to VCR::Cassette.new to check the options passed to the cassette and raise an error if any invalid options are passed. * Optimized ERB rendering a bit. Rather than creating a new struct subclass for each time we render an ERB cassette with locals, we keep a cache of reusable struct subclasses based on the desired attributes. [Benchmarking](http://gist.github.com/512948) reveals this is about 28% faster. * Upgraded tests to use em-http-request 0.2.10 rather than 0.2.7. ## 1.0.3 (August 5, 2010) [Full Changelog](http://github.com/vcr/vcr/compare/v1.0.2...v1.0.3) * Upgraded VCR specs to RSpec 2. * Updated `VCR::CucumberTags` so that it uses an `around` hook rather than a `before` hook and an `after` hook. Around hooks were added to Cucumber in the 0.7.3 release, so you'll have to be on that version or higher to use the `VCR::CucumberTags` feature. * Updated the WebMock version requirement to 1.3.3 or greater. 1.3.2 and earlier versions did not properly handle multiple value for the same response header. * Miscellaneous documentation updates. ## 1.0.2 (July 6, 2010) [Full Changelog](http://github.com/vcr/vcr/compare/v1.0.1...v1.0.2) * Fixed VCR to work with [rest-client](http://github.com/archiloque/rest-client). Rest-client extends the Net::HTTP response body string with a module containing additional data, which got serialized to the cassette file YAML and occasionally caused problems when the YAML was deserialized. Bug reported by [Thibaud Guillaume-Gentil](http://github.com/thibaudgg). * Setup bundler to manage development dependencies. ## 1.0.1 (July 1, 2010) [Full Changelog](http://github.com/vcr/vcr/compare/v1.0.0...v1.0.1) * Fixed specs and features so they pass on MRI 1.9.2-preview3 and JRuby 1.5.1. * Normalized response and request headers so that they are stored the same (i.e. lower case keys, arrays of values) in the cassette yaml files, regardless of which HTTP library is used. This is the same way Net::HTTP normalizes HTTP headers. * Fixed `VCR.use_cassette` so that it doesn't eject a cassette if an exception occurs while inserting one. * Fixed FakeWeb adapter so that it works for requests that use basic auth. Patch submitted by [Eric Allam](http://github.com/rubymaverick). ## 1.0.0 (June 22, 2010) [Full Changelog](http://github.com/vcr/vcr/compare/v0.4.1...v1.0.0) * New Features * Added support for [HTTPClient](http://github.com/nahi/httpclient), [Patron](http://github.com/toland/patron) and [em-http-request](http://github.com/igrigorik/em-http-request) when WebMock is used. Any future http libraries WebMock supports should (theoretically, at least) work without any VCR code changes. Thanks to [Bartosz Blimke](http://github.com/bblimke) for adding the necessary code to WebMock to make this happen! * Added support for dynamic responses using ERB. A cassette will be evaluated as ERB before the YAML is deserialized if you pass it an `:erb => true` option. You can pass variables using `:erb => { :var1 => 'some value', :var2 => 'another value' }`. * Added `ignore_localhost` configuration setting, which defaults to false. Setting it true does the following: * Localhost requests will proceed as normal. The "Real HTTP connections are disabled" error will not occur. * Localhost requests will not be recorded. * Previously recorded localhost requests will not be replayed. * Exposed the version number: * `VCR.version` => string (in the format "major.minor.patch") * `VCR.version.parts` => array of integers * `VCR.version.major` => integer * `VCR.version.minor` => integer * `VCR.version.patch` => integer * Added test coverage and documentation of using a regex for non-deterministic URLs (i.e. URLs that include a timestamp as a query parameter). It turns out this feature worked before, and I just didn't realize it :). * Breaking Changes * The `:allow_real_http => lambda { |uri| ... }` cassette option has been removed. There was no way to get this to work with the newly supported http libraries without extensive monkeypatching, and it was mostly useful for localhost requests, which is more easily handled by the new `ignore_localhost` config setting. * Removed methods and options that had been previously deprecated. If you're upgrading from an old version, I recommend upgrading to 0.4.1 first, deal with all the deprecation warnings, then upgrade to 1.0.0. * Misc Changes: * Removed dependency on [jeweler](http://github.com/technicalpickles/jeweler). Manage the gemspec by hand instead. * Removed some extensions that are no longer necessary. ## 0.4.1 May 11, 2010 [Full Changelog](http://github.com/vcr/vcr/compare/v0.4.0...v0.4.1) * Fixed a bug: when `Net::HTTPResponse#read_body` was called after VCR had read the body to record a new request, it raised an error (`IOError: Net::HTTPResponse#read_body called twice`). My fix extends Net::HTTPResponse so that it no longer raises this error. ## 0.4.0 April 28, 2010 [Full Changelog](http://github.com/vcr/vcr/compare/v0.3.1...v0.4.0) * Added support for webmock. All the fakeweb-specific code is now in an adapter (as is the webmock code). * Changed the format of the VCR cassettes. The old format was tied directly to Net::HTTP, but webmock supports other HTTP libraries and I plan to allow VCR to use them in the future. Note that this is a breaking change--your old VCR cassettes from prior releases will not work with VCR 0.4.0. However, VCR provides a rake task to assist you in migrating your cassettes to the new format. Simply add `load 'vcr/tasks/vcr.rake'` to your project's Rakefile, and run: ``` $ rake vcr:migrate_cassettes DIR=path/to/cassete/library/directory ``` * The new cassette format records more information about the request (i.e. the request headers and body), so that it can potentially be used with webmock in the future. * Made most of `VCR::Cassette`'s methods private. I had forgotten to make the methods private before, and most of them don't need to be exposed. * Automatically disallow http connections using the appropriate setting of the http stubbing library (fakeweb or webmock). This relieves users from the need to set the option themselves, so they hopefully aren't using either fakeweb or webmock directly, making it much easier to switch between these. * Change documentation from rdoc to markdown format. * Lots of other refactoring. ## 0.3.1 April 10, 2010 [Full Changelog](http://github.com/vcr/vcr/compare/v0.3.0...v0.3.1) * Fixed a bug: when `Net::HTTP#request` was called with a block that had a return statement, the response was not being recorded. ## 0.3.0 March 24, 2010 [Full Changelog](http://github.com/vcr/vcr/compare/v0.2.0...v0.3.0) * Renamed a bunch of methods, replacing them with method names that more clearly fit the VCR/cassette metaphor: * `VCR.create_cassette!` => `VCR.insert_cassette` * `VCR.destroy_cassette!` => `VCR.eject_cassette` * `VCR.with_cassette` => `VCR.use_cassette` * `VCR::Cassette#destroy!` => `VCR::Cassette#eject` * `VCR::Cassette#cache_file` => `VCR::Cassette#file` * `VCR::Config.cache_dir` => `VCR::Config.cassette_library_dir` * `:unregistered` record mode => `:new_episodes` record mode * All the old methods still work, but you'll get deprecation warnings. ## 0.2.0 March 9, 2010 [Full Changelog](http://github.com/vcr/vcr/compare/v0.1.2...v0.2.0) * Added `:allow_real_http` cassette option, which allows VCR to work with capybara and a javascript driver. Bug reported by [Ben Hutton](http://github.com/benhutton). * Deprecated the `default_cassette_record_mode` option. Use `default_cassette_options[:record_mode]` instead. ## 0.1.2 March 4, 2010 [Full Changelog](http://github.com/vcr/vcr/compare/v0.1.1...v0.1.2) * Added explanatory note about VCR to `FakeWeb::NetConnectNotAllowedError#message`. * Got things to work for when a cassette records multiple requests made to the same URL with the same HTTP verb, but different responses. We have to register an array of responses with fakeweb. * Fixed our `Net::HTTP` monkey patch so that it only stores the recorded response once per request. Internally, `Net::HTTP#request` recursively calls itself (passing slightly different arguments) in certain circumstances. ## 0.1.1 February 25, 2010 [Full Changelog](http://github.com/vcr/vcr/compare/v0.1.0...v0.1.1) * Handle asynchronous HTTP requests (such as for mechanize). Bug reported by [Thibaud Guillaume-Gentil](http://github.com/thibaudgg). ## 0.1.0 February 25, 2010 [Full Changelog](http://github.com/vcr/vcr/compare/d2577f79247d7db60bf160881b1b64e9fa10e4fd...v0.1.0) * Initial release. Basic recording and replaying of responses works. vcr-5.0.0/CONTRIBUTING.md000066400000000000000000000015531347305653000145510ustar00rootroot00000000000000## Contributing Install [bundler](http://gembundler.com/) and use it to install all the development dependencies: ```console gem install bundler bundle install ``` You should be able to run the tests now: ```console bundle exec rake ``` VCR uses [RSpec](http://github.com/rspec/rspec) for unit tests. The specs are written in a very "focused" style, where each spec is concerned only with exercising the object under test, using mocks as necessary. You can run the specs using `rake spec`. [Cucumber](http://cukes.info/) is used for end-to-end full stack integration tests that also function as VCR's documentation. ## Problems running bundle install? If you get an error while running `bundle install`, it may be one of the "extras" gems which are not required for development. Try installing it without these gems. ```console bundle install --without extras ``` vcr-5.0.0/Gemfile000066400000000000000000000004221347305653000136050ustar00rootroot00000000000000source 'https://rubygems.org' gem "faraday", ">= 0.9.2" gemspec gem 'jruby-openssl', :platforms => :jruby platform :mri do gem "typhoeus", "~> 1.1.0" gem "patron", "0.6.3" gem "em-http-request" gem "curb", "~> 0.8.8" end platform :ruby do gem "yajl-ruby" end vcr-5.0.0/Gemfile.cucumber-1.3000066400000000000000000000001041347305653000157050ustar00rootroot00000000000000source "https://rubygems.org" gem "cucumber", "~> 1.3.20" gemspec vcr-5.0.0/Gemfile.faraday-0.11000066400000000000000000000001711347305653000155710ustar00rootroot00000000000000source "https://rubygems.org" gem "faraday", "~> 0.11.0" gemspec platform :mri do gem "typhoeus" gem "patron" end vcr-5.0.0/Gemfile.typhoeus-0.4000066400000000000000000000001361347305653000157650ustar00rootroot00000000000000source "https://rubygems.org" gem "typhoeus", "~> 0.4.2" gem "webmock", "~> 1.8.11" gemspec vcr-5.0.0/LICENSE000066400000000000000000000020461347305653000133230ustar00rootroot00000000000000Copyright (c) 2010-2015 Myron Marston Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. vcr-5.0.0/MAINTAINERS000066400000000000000000000000621347305653000140070ustar00rootroot00000000000000krainboltgreene mcfiredrill galori mislav ecnalyr vcr-5.0.0/README.md000066400000000000000000000546761347305653000136150ustar00rootroot00000000000000vcr === [![Quality](https://img.shields.io/codeclimate/github/vcr/vcr.svg?style=flat-square)](https://codeclimate.com/github/vcr/vcr) [![Coverage](https://img.shields.io/codeclimate/coverage/github/vcr/vcr.svg?style=flat-square)](https://codeclimate.com/github/vcr/vcr) [![Build](https://img.shields.io/travis-ci/vcr/vcr.svg?style=flat-square)](https://travis-ci.org/vcr/vcr) [![Releases](https://img.shields.io/github/release/vcr/vcr.svg?style=flat-square)](http://github.com/vcr/vcr/releases) [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](http://opensource.org/licenses/MIT) [![Version](https://img.shields.io/gem/v/vcr.svg?style=flat-square)](https://rubygems.org/gems/vcr) [![OpenCollective](https://opencollective.com/vcr/backers/badge.svg)](https://opencollective.com/vcr#backer) [![OpenCollective](https://opencollective.com/vcr/sponsors/badge.svg)](https://opencollective.com/vcr#sponsor) Record your test suite's HTTP interactions and replay them during future test runs for fast, deterministic, accurate tests. **Help Wanted** We're looking for new maintainers. If you'd like to help maintain a well-used gem please create an issue so we can add you. Usage ===== ``` ruby require 'rubygems' require 'test/unit' require 'vcr' VCR.configure do |config| config.cassette_library_dir = "fixtures/vcr_cassettes" config.hook_into :webmock end class VCRTest < Test::Unit::TestCase def test_example_dot_com VCR.use_cassette("synopsis") do response = Net::HTTP.get_response(URI('http://www.iana.org/domains/reserved')) assert_match /Example domains/, response.body end end end ``` Run this test once, and VCR will record the HTTP request to `fixtures/vcr_cassettes/synopsis.yml`. Run it again, and VCR will replay the response from iana.org when the HTTP request is made. This test is now fast (no real HTTP requests are made anymore), deterministic (the test will continue to pass, even if you are offline, or iana.org goes down for maintenance) and accurate (the response will contain the same headers and body you get from a real request). You can use a different cassette library directory (e.g., "test/vcr_cassettes"). **Rails and Minitest:** Do *not* use 'test/fixtures' as the directory if you're using Rails and Minitest (Rails will instead transitively load any files in that directory as models). **Features** * Automatically records and replays your HTTP interactions with minimal setup/configuration code. * Supports and works with the HTTP stubbing facilities of multiple libraries. Currently, the following are supported: * [WebMock](https://github.com/bblimke/webmock) * [Typhoeus](https://github.com/typhoeus/typhoeus) * [Faraday](https://github.com/lostisland/faraday) * [Excon](https://github.com/excon/excon) * Supports multiple HTTP libraries: * [Patron](https://github.com/toland/patron) (when using WebMock) * [Curb](https://github.com/taf2/curb) (when using WebMock -- only supports Curl::Easy at the moment) * [HTTPClient](https://github.com/nahi/httpclient) (when using WebMock) * [em-http-request](https://github.com/igrigorik/em-http-request) (when using WebMock) * [Net::HTTP](http://www.ruby-doc.org/stdlib/libdoc/net/http/rdoc/index.html) (when using WebMock) * [Typhoeus](https://github.com/typhoeus/typhoeus) (Typhoeus::Hydra, but not Typhoeus::Easy or Typhoeus::Multi) * [Excon](https://github.com/geemus/excon) * [Faraday](https://github.com/lostisland/faraday) * And of course any library built on Net::HTTP, such as [Mechanize](https://github.com/sparklemotion/mechanize), [HTTParty](https://github.com/jnunemaker/httparty) or [Rest Client](https://github.com/rest-client/rest-client). * Request matching is configurable based on HTTP method, URI, host, path, body and headers, or you can easily implement a custom request matcher to handle any need. * The same request can receive different responses in different tests--just use different cassettes. * The recorded requests and responses are stored on disk in a serialization format of your choice (currently YAML and JSON are built in, and you can easily implement your own custom serializer) and can easily be inspected and edited. * Dynamic responses are supported using ERB. * Optionally re-records cassettes on a configurable regular interval to keep them fresh and current. * Disables all HTTP requests that you don't explicitly allow. * Simple Cucumber integration is provided using tags. * Includes convenient RSpec macros and integration with RSpec 2 metadata. * Known to work well with many popular Ruby libraries including RSpec 1 & 2, Cucumber, Test::Unit, Capybara, Mechanize, Rest Client and HTTParty. * Includes Rack and Faraday middleware. * Supports filtering sensitive data from the response body The docs come in two flavors: * The [relish docs](https://relishapp.com/vcr/vcr/docs) contain example-based documentation (VCR's Cucumber suite, in fact). It's a good place to look when you are first getting started with VCR, or if you want to see an example of how to use a feature. * The [rubydoc.info docs](http://www.rubydoc.info/gems/vcr/frames) contain API documentation. The API docs contain detailed info about all of VCR's public API. * See the [Upgrade](https://github.com/vcr/vcr/blob/master/Upgrade.md) doc for info about what's new and changed in VCR 2.0. There is also a Railscast, which will get you up and running in no-time http://railscasts.com/episodes/291-testing-with-vcr. **Release Policy** VCR follows the principles of [semantic versioning](http://semver.org/). The [API documentation](http://rubydoc.info/gems/vcr/frames) defines VCR's public API. Patch level releases contain only bug fixes. Minor releases contain backward-compatible new features. Major new releases contain backwards-incompatible changes to the public API. **Ruby Interpreter Compatibility** VCR has been tested on the following ruby interpreters: * MRI 1.9.3 * MRI 2.0.0 * MRI 2.1 * MRI 2.2 * MRI 2.3.0 * JRuby * Rubinius Note that as of VCR 3, 1.8.7 and 1.9.2 are not supported. **Development** * Source hosted on [GitHub](http://github.com/vcr/vcr). * Direct questions and discussions to the [mailing list](http://groups.google.com/group/vcr-ruby). * Report issues on [GitHub Issues](http://github.com/vcr/vcr/issues). * Pull requests are very welcome! Please include spec and/or feature coverage for every patch, and create a topic branch for every separate change you make. * See the [Contributing](https://github.com/vcr/vcr/blob/master/CONTRIBUTING.md) guide for instructions on running the specs and features. * Code quality metrics are checked by [Code Climate](https://codeclimate.com/github/vcr/vcr). * Documentation is generated with [YARD](http://yardoc.org/) ([cheat sheet](http://cheat.errtheblog.com/s/yard)). To generate while developing: ``` yard server --reload ``` **Ports in Other Languages** * [Betamax](https://github.com/sigmavirus24/betamax) (Python) * [VCR.py](https://github.com/kevin1024/vcrpy) (Python) * [Betamax](https://github.com/thegreatape/betamax) (Go) * [DVR](https://github.com/orchestrate-io/dvr) (Go) * [Go VCR](https://github.com/dnaeon/go-vcr) (Go) * [Betamax](https://github.com/wjlroe/betamax) (Clojure) * [vcr-clj](https://github.com/ifesdjeen/vcr-clj) (Clojure) * [scotch](https://github.com/mleech/scotch) (C#/.NET) * [Betamax.NET](https://github.com/mfloryan/Betamax.Net) (C#/.NET) * [ExVCR](https://github.com/parroty/exvcr) (Elixir) * [HAVCR](https://github.com/cordawyn/havcr) (Haskell) * [Mimic](https://github.com/acoulton/mimic) (PHP/Kohana) * [PHP-VCR](https://github.com/php-vcr/php-vcr) (PHP) * [Nock-VCR](https://github.com/carbonfive/nock-vcr) (JavaScript/Node) * [Sepia](https://github.com/linkedin/sepia) (JavaScript/Node) * [VCR.js](https://github.com/elcuervo/vcr.js) (JavaScript) * [yakbak](https://github.com/flickr/yakbak) (JavaScript/Node) * [NSURLConnectionVCR](https://bitbucket.org/martijnthe/nsurlconnectionvcr) (Objective-C) * [VCRURLConnection](https://github.com/dstnbrkr/VCRURLConnection) (Objective-C) * [DVR](https://github.com/venmo/DVR) (Swift) * [VHS](https://github.com/diegoeche/vhs) (Erlang) * [Betamax](https://github.com/betamaxteam/betamax) (Java) * [http_replayer](https://github.com/ucarion/http_replayer) (Rust) * [OkReplay](https://github.com/airbnb/okreplay) (Java/Android) * [vcr](https://github.com/ropensci/vcr) (R) **Related Projects** * [Mr. Video](https://github.com/quidproquo/mr_video) (Rails engine for managing VCR cassettes and episodes) **Similar Libraries in Ruby** * [Ephemeral Response](https://github.com/sandro/ephemeral_response) * [Net::HTTP Spy](https://github.com/martinbtt/net-http-spy) * [NetRecorder](https://github.com/chrisyoung/netrecorder) * [Puffing Billy](https://github.com/oesmith/puffing-billy) * [REST-assured](https://github.com/artemave/REST-assured) * [Stale Fish](https://github.com/jsmestad/stale_fish) * [WebFixtures](https://github.com/trydionel/web_fixtures) Credits ======= * [Aslak Hellesøy](https://github.com/aslakhellesoy) for [Cucumber](https://github.com/aslakhellesoy/cucumber). * [Bartosz Blimke](https://github.com/bblimke) for [WebMock](https://github.com/bblimke/webmock). * [Chris Kampmeier](https://github.com/chrisk) for [FakeWeb](https://github.com/chrisk/fakeweb). * [Chris Young](https://github.com/chrisyoung) for [NetRecorder](https://github.com/chrisyoung/netrecorder), the inspiration for VCR. * [David Balatero](https://github.com/dbalatero) and [Hans Hasselberg](https://github.com/i0rek) for help with [Typhoeus](https://github.com/typhoeus/typhoeus) support. * [Wesley Beary](https://github.com/geemus) for help with [Excon](https://github.com/geemus/excon) support. * [Jacob Green](https://github.com/Jacobkg) for help with ongoing VCR maintenance. * [Jan Berdajs](https://github.com/mrbrdo) and [Daniel Berger](https://github.com/djberg96) for improvements to thread-safety. Thanks also to the following people who have contributed patches or helpful suggestions: * [Aaron Brethorst](https://github.com/aaronbrethorst) * [Alexander Wenzowski](https://github.com/wenzowski) * [Austen Ito](https://github.com/austenito) * [Avdi Grimm](https://github.com/avdi) * [Bartosz Blimke](http://github.com/bblimke) * [Benjamin Oakes](https://github.com/benjaminoakes) * [Ben Hutton](https://github.com/benhutton) * [Bradley Isotope](https://github.com/bradleyisotope) * [Carlos Kirkconnell](https://github.com/kirkconnell) * [Chad Jolly](https://github.com/cjolly) * [Chris Le](https://github.com/chrisle) * [Chris Gunther](https://github.com/cgunther) * [Eduardo Maia](https://github.com/emaiax) * [Eric Allam](https://github.com/rubymaverick) * [Ezekiel Templin](https://github.com/ezkl) * [Flaviu Simihaian](https://github.com/closedbracket) * [Gordon Wilson](https://github.com/gordoncww) * [Hans Hasselberg](https://github.com/i0rek) * [Herman Verschooten](https://github.com/Hermanverschooten) * [Ian Cordasco](https://github.com/sigmavirus24) * [Ingemar](https://github.com/ingemar) * [Ilya Scharrenbroich](https://github.com/quidproquo) * [Jacob Green](https://github.com/Jacobkg) * [James Bence](https://github.com/jbence) * [Jay Shepherd](https://github.com/jayshepherd) * [Jeff Pollard](https://github.com/Fluxx) * [Joe Nelson](https://github.com/begriffs) * [Jonathan Tron](https://github.com/JonathanTron) * [Justin Smestad](https://github.com/jsmestad) * [Karl Baum](https://github.com/kbaum) * [Kris Luminar](https://github.com/kris-luminar) * [Kurt Funai](https://github.com/kurtfunai) * [Luke van der Hoeven](https://github.com/plukevdh) * [Mark Burns](https://github.com/markburns) * [Max Riveiro](https://github.com/kavu) * [Michael Lavrisha](https://github.com/vrish88) * [Michiel de Mare](https://github.com/mdemare) * [Mike Dalton](https://github.com/kcdragon) * [Mislav Marohnić](https://github.com/mislav) * [Nathaniel Bibler](https://github.com/nbibler) * [Noah Davis](https://github.com/noahd1) * [Oliver Searle-Barnes](https://github.com/opsb) * [Omer Rauchwerger](https://github.com/rauchy) * [Paco Guzmán](https://github.com/pacoguzman) * [Paul Morgan](https://github.com/jumanjiman) * [playupchris](https://github.com/playupchris) * [Ron Smith](https://github.com/ronwsmith) * [Ryan Bates](https://github.com/ryanb) * [Ryan Burrows](https://github.com/rhburrows) * [Ryan Castillo](https://github.com/rmcastil) * [Sathya Sekaran](https://github.com/sfsekaran) * [Scott Carleton](https://github.com/ScotterC) * [Shay Frendt](https://github.com/shayfrendt) * [Steve Faulkner](https://github.com/southpolesteve) * [Stephen Anderson](https://github.com/bendycode) * [Todd Lunter](https://github.com/tlunter) * [Tyler Hunt](https://github.com/tylerhunt) * [Uģis Ozols](https://github.com/ugisozols) * [vzvu3k6k](https://github.com/vzvu3k6k) * [Wesley Beary](https://github.com/geemus) # Backers Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/vcr#backer)] # Sponsors Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/vcr#sponsor)] Copyright ========= Copyright (c) 2010-2018 Myron Marston Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. vcr-5.0.0/Rakefile000066400000000000000000000066111347305653000137650ustar00rootroot00000000000000#!/usr/bin/env rake require "bundler/gem_tasks" require "rspec/core/rake_task" desc "Run all the tests in spec" RSpec::Core::RakeTask.new(:spec) using_git = File.exist?(File.expand_path('../.git/', __FILE__)) require "cucumber/rake/task" Cucumber::Rake::Task.new desc "Default: run tests" task default: [:spec, :cucumber] def ensure_relish_doc_symlinked(filename) from_filename = filename.dup from_filename << '.md' unless filename =~ /\.md$/ from = File.expand_path("../features/#{from_filename}", __FILE__) to = File.expand_path("../#{filename}", __FILE__) if File.symlink?(from) return if File.readlink(from) == to # delete the old symlink File.unlink(from) end FileUtils.ln_s to, from end desc "Push cukes to relishapp using the relish-client-gem" task :relish do unless ENV['SKIP_RELISH'] %w[ README.md CHANGELOG.md Upgrade.md LICENSE CONTRIBUTING.md].each do |file| ensure_relish_doc_symlinked(file) end require 'vcr/version' sh "relish versions:add vcr/vcr:#{VCR.version}" if ENV['NEW_RELISH_RELEASE'] == 'true' sh "relish push vcr/vcr:#{VCR.version}" end end task :prep_relish_release do ENV['NEW_RELISH_RELEASE'] ||= 'true' end task :release => [:prep_relish_release, :relish] load './lib/vcr/tasks/vcr.rake' namespace :vcr do task :reset_spec_cassettes do ENV['DIR'] = 'spec/fixtures' def VCR.version; "2.0.0"; end sh "git checkout v2.0.0.beta1 -- spec/fixtures" end task :migrate_cassettes => :reset_spec_cassettes end desc "Migrate cucumber cassettes" task :migrate_cucumber_cassettes do require 'vcr' require 'ruby-debug' VCR.configure do |c| c.cassette_library_dir = 'tmp/migrate' c.default_cassette_options = { :serialize_with => :syck } end # We want 2.0.0 in the cucumber cassettes instead of 2.0.0.rc1 def VCR.version "2.0.0" end Dir["features/**/*.feature"].each do |feature_file| # The ERB cassettes can't be migrated automatically. next if feature_file.include?('dynamic_erb') puts " - Migrating #{feature_file}" contents = File.read(feature_file) # http://rubular.com/r/gjzkoaYX2O contents.scan(/:\n^\s+"""\n([\s\S]+?)"""/).each do |captures| capture = captures.first indentation = capture[/^ +/] cassette_yml = capture.gsub(/^#{indentation}/, '') new_yml = nil file_name = "tmp/migrate/cassette.yml" File.open(file_name, 'w') { |f| f.write(cassette_yml) } cassette = VCR::Cassette.new('cassette') hash = begin cassette.serializable_hash rescue => e puts " Skipping #{capture[0, 80]}" next end new_yml = VCR::Cassette::Serializers::Syck.serialize(hash) new_yml.gsub!(/^/, indentation) new_yml << indentation new_yml.gsub!(/^\s+\n(\s+response:)/, '\1') contents.gsub!(capture, new_yml) end File.open(feature_file, 'w') { |f| f.write(contents) } end end desc "Run the last cuke directly" task :run_last_cuke do command = ENV.fetch('CMD') do raise "Must pass CMD" end Dir.chdir("tmp/aruba") do sh "RUBYOPT='-I.:../../lib -r../../features/support/vcr_cucumber_helpers' ruby #{command}" end end desc "Boot test app" task :boot_test_app do require './spec/support/vcr_localhost_server' require './spec/support/sinatra_app' VCR::SinatraApp.boot puts "Booted sinatra app on port: #{VCR::SinatraApp.port}" loop { } puts "Shutting down." end vcr-5.0.0/Upgrade.md000066400000000000000000000174721347305653000142400ustar00rootroot00000000000000See the [Changelog](CHANGELOG.md) for a complete list of changes from VCR 1.x to 2.0. This file simply lists the most pertinent ones to upgrading. ## Supported Rubies Ruby 1.8.6 and 1.9.1 are no longer supported. ## Configuration Changes In VCR 1.x, your configuration block would be something like this: ``` ruby VCR.config do |c| c.cassette_library_dir = 'cassettes' c.stub_with :fakeweb, :typhoeus end ``` This will continue to work in VCR 2.0 but will generate deprecation warnings. Instead, you should change this to: ``` ruby VCR.configure do |c| c.cassette_library_dir = 'cassettes' c.hook_into :fakeweb, :typhoeus end ``` ## New Cassette Format The cassette format has changed between VCR 1.x and VCR 2.0. VCR 1.x cassettes cannot be used with VCR 2.0. The easiest way to upgrade is to simply delete your cassettes and re-record all of them. VCR also provides a rake task that attempts to upgrade your 1.x cassettes to the new 2.0 format. To use it, add the following line to your Rakefile: ``` ruby load 'vcr/tasks/vcr.rake' ``` Then run `rake vcr:migrate_cassettes DIR=path/to/your/cassettes/directory` to upgrade your cassettes. Note that this rake task may be unable to upgrade some cassettes that make extensive use of ERB. In addition, now that VCR 2.0 does less normalization then before, it may not be able to migrate the cassette perfectly. It's recommended that you delete and re-record your cassettes if you are able. ## Custom Request Matchers VCR 2.0 allows you to register custom request matchers: ``` ruby 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 ``` You can also pass any callable (an object that responds to #call, such as a lambda) to the `:match_requests_on` option: ``` ruby port_matcher = lambda do |request_1, request_2| URI(request_1.uri).port == URI(request_2.uri).port end VCR.use_cassette("example", :match_requests_on => [:host, port_matcher, :method]) do # make an HTTP request end ``` In addition, a helper method is provided for generating a custom matcher that ignores one or more query parameters: ``` ruby uri_without_timestamp = VCR.request_matchers.uri_without_param(:timestamp) VCR.configure do |c| c.register_request_matcher(:uri_without_timestamp, &uri_without_timestamp) end ``` ## Custom Serializers VCR 2.0 supports multiple serializers. `:yaml`, `:json`, `:psych` and `:syck` are supported out of the box, and it's easy to implement your own. Custom serializers must implement `#file_extension`, `#serialize` and `#deserialize`: ``` ruby VCR.use_cassette("example", :serialize_with => :json) do # make an HTTP request end marshal_serializer = Object.new marshal_serializer.instance_eval do def file_extension "marsh" end def serialize(hash) Marshal.dump(hash) end def deserialize(string) Marshal.load(string) end end VCR.configure do |c| c.cassette_serializers[:marshal] = marshal_serializer c.default_cassette_options = { :serialize_with => :marshal } end ``` ## Request Hooks VCR 2.0 has new request hooks, allowing you to inject custom logic before an HTTP request, after an HTTP request, or around an HTTP request: ``` ruby VCR.configure do |c| c.before_http_request do |request| # do something with the request end c.after_http_request do |request, response| # do something with the request or response end # around_http_request only works on ruby 1.9 c.around_http_request do |request| uri = URI(request.uri) if uri.host == 'api.geocoder.com' # 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.query.split('=').last) VCR.use_cassette("geocoding/#{address}", &request) else request.proceed end end end ``` ## Ignore a Request Based on Anything You can now define what requests get ignored using a block. This gives you the flexibility to ignore a requets based on anything. ``` ruby VCR.configure do |c| c.ignore_request do |request| uri = URI(request.uri) uri.host == 'localhost' && uri.port == 7500 end end ``` ## Integration with RSpec 2 Metadata VCR can integrate directly with RSpec metadata: ``` ruby VCR.configure do |c| c.configure_rspec_metadata! end RSpec.configure do |c| # so we can use `:vcr` rather than `:vcr => true`; # in RSpec 3 this will no longer be necessary. c.treat_symbols_as_metadata_keys_with_true_values = true end # apply it to an example group describe MyAPIWrapper, :vcr do end describe MyAPIWrapper do # apply it to an individual example it "does something", :vcr do end # set some cassette options it "does something", :vcr => { :record => :new_episodes } do end # override the cassette name it "does something", :vcr => { :cassette_name => "something" } do end end ``` ## Improved Faraday Integration VCR 1.x integrated with Faraday but required that you insert `VCR::Middleware::Faraday` into your middleware stack and configure `stub_with :faraday`. VCR 2 now takes care of inserting itself into the Faraday middleware stack if you configure `hook_into :faraday`. ## Improved Unhandled Error Messages When VCR is unsure how to handle a request, the error message now contains suggestions for how you can configure VCR or your test so it can handle the request. ## Debug Logger VCR 2.0 has a new configuration option that will turn on a logging mode so you can get more insight into what VCR is doing, for troubleshooting purposes: ``` ruby VCR.configure do |c| c.debug_logger = File.open('log/vcr.log', 'w') # or... c.debug_logger = $stderr end ``` ## Playback Changes In VCR 1.x, a single HTTP interaction could be played back multiple times. This was mostly due to how VCR was implemented using FakeWeb and WebMock, and was not really by design. It's more in keeping with the philosophy of VCR to record the entire sequence of HTTP interactions (including the duplicate requests). In VCR 2, each recorded HTTP interaction can only be played back once unless you use the new `:allow_playback_repeats` option. In VCR 1.x, request matching was delegated to the HTTP stubbing library (typically FakeWeb or WebMock). They contain some normalization logic that can treat some URIs that are different strings as equivalent. For example, WebMock ignores the ordering of query parameters: ``` ruby > require 'webmock' => true > uri1 = "http://foo.com/bar?a=1&b=2" => "http://foo.com/bar?a=1&b=2" > uri2 = "http://foo.com/bar?b=2&a=1" => "http://foo.com/bar?b=2&a=1" > uri1 == uri2 => false > WebMock::Util::URI.normalize_uri(uri1) == WebMock::Util::URI.normalize_uri(uri2) => true ``` VCR 2, the `:uri` matcher simply [uses string equality](https://github.com/myronmarston/vcr/blob/v2.0.0/lib/vcr/request_matcher_registry.rb#L111). This means that there are some cases of non-deterministic URIs that VCR 1.x matched but VCR 2.0 will not match. If you need the `:uri` matcher to be tolerant of slight variations like these, you can easily override it: ``` ruby VCR.configure do |c| c.register_request_matcher(:uri) do |r1, r2| WebMock::Util::URI.normalize_uri(r1.uri) == WebMock::Util::URI.normalize_uri(r2.uri) end end ``` ## Preserve Exact Body Bytes Sometimes the request or response body of an HTTP interaction cannot be serialized and deserialized properly. Usually this is due to the body having invalid UTF-8 bytes. This new option configures VCR to base64 encode the body in order to preserve the bytes exactly. It can either be configured globally with a block, or set on individual cassettes: ``` ruby 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 # or.... VCR.use_cassette("my_cassette", :preserve_exact_body_bytes => true) do # ... end ``` vcr-5.0.0/cucumber.yml000066400000000000000000000015221347305653000146440ustar00rootroot00000000000000<% base_opts = '--require features/step_definitions --require features/support' std_opts = "--format progress --strict" std_opts << " --backtrace" if ENV["CI"] rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" rerun_opts = rerun.to_s.strip.empty? ? "--format progress features" : "--format pretty #{rerun}" exclusions = [] exclusions << "--tags ~@exclude-#{RUBY_VERSION.split('.').first(2).join}" exclusions << "--tags ~@exclude-#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}" exclusions << "--tags ~@exclude-#{RUBY_ENGINE}" if defined?(RUBY_ENGINE) exclusions = exclusions.join(' ') %> default: <%= base_opts %> <%= std_opts %> --tags ~@wip <%= exclusions %> features wip: <%= base_opts %> --tags @wip <%= exclusions %> features rerun: <%= base_opts %> <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip <%= exclusions %> vcr-5.0.0/features/000077500000000000000000000000001347305653000141325ustar00rootroot00000000000000vcr-5.0.0/features/.nav000066400000000000000000000030341347305653000147170ustar00rootroot00000000000000- getting_started.md (Getting Started) - Upgrade.md (Upgrade) - CHANGELOG.md (Changelog) - about_these_examples.md (About These Examples) - LICENSE.md (License) - CONTRIBUTING.md (Contributing) - cassettes: - format.feature - naming.feature - no_cassette.feature - dynamic_erb.feature - automatic_re_recording.feature - exclusive.feature - update_content_length_header.feature - decompress.feature - persistence.feature - allow_unused_http_interactions.feature - record_modes: - once.feature - new_episodes.feature - none.feature - all.feature - configuration: - cassette_library_dir.feature - hook_into.feature - default_cassette_options.feature - ignore_request.feature - filter_sensitive_data.feature - allow_http_connections_when_no_cassette.feature - debug_logging.feature - preserve_exact_body_bytes.feature - uri_parser.feature - query_parser.feature - hooks: - before_record.feature - before_playback.feature - before_http_request.feature - after_http_request.feature - around_http_request.feature - request_matching: - method.feature - uri.feature - host.feature - path.feature - query.feature - body.feature - headers.feature - identical_request_sequence.feature - custom_matcher.feature - uri_without_param.feature - playback_repeats.feature - test_frameworks: - test_unit.feature - rspec_metadata.feature - rspec_macro.feature - cucumber.feature - middleware: - rack.feature - faraday.feature - http_libraries: - net_http.feature - em_http_request.feature vcr-5.0.0/features/about_these_examples.md000066400000000000000000000016621347305653000206610ustar00rootroot00000000000000The cucumber features provided here demonstrate all of the major features of VCR. These features are executable documentation for VCR. Many of the examples use one (or both) of these helper functions provided by `spec/support/cucumber_helpers.rb`: * `start_sinatra_app`: starts a sinatra application on the given port. The server automatically shuts down when the ruby script ends. Many examples re-run the script without the sinatra server to demonstrate the replaying of a recorded HTTP response. * `include_http_adapter_for`: includes a module that implements a common HTTP interface for the given HTTP library. The `response_body_for` method will make an HTTP request using the given library. This allows scenarios to be run against each different supported HTTP library. If you have ideas to clarify or improve any of these cucumber features, please submit an [issue](https://github.com/vcr/vcr/issues) or pull request. vcr-5.0.0/features/cassettes/000077500000000000000000000000001347305653000161305ustar00rootroot00000000000000vcr-5.0.0/features/cassettes/allow_unused_http_interactions.feature000066400000000000000000000057521347305653000260400ustar00rootroot00000000000000Feature: Allow Unused HTTP Interactions If set to false, this cassette option will cause VCR to raise an error when a cassette is ejected and there are unused HTTP interactions remaining, unless there is already an exception unwinding the callstack. It verifies that all requests included in the cassette were made, and allows VCR to function a bit like a mock object at the HTTP layer. The option defaults to true (mostly for backwards compatibility). Background: Given a file named "vcr_config.rb" with: """ruby require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' end """ And a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: get uri: http://example.com/foo body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "5" body: encoding: UTF-8 string: Hello http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Scenario: Unused HTTP interactions are allowed by default Given a file named "allowed_by_default.rb" with: """ruby require 'vcr_config' VCR.use_cassette("example") do # no requests end """ When I run `ruby allowed_by_default.rb` Then it should pass Scenario: Error raised if option is false and there are unused interactions Given a file named "disallowed_with_no_requests.rb" with: """ruby require 'vcr_config' VCR.use_cassette("example", :allow_unused_http_interactions => false) do # no requests end """ When I run `ruby disallowed_with_no_requests.rb` Then it should fail with an error like: """ There are unused HTTP interactions left in the cassette: - [get http://example.com/foo] => [200 "Hello"] """ Scenario: No error raised if option is false and all interactions are used Given a file named "disallowed_with_all_requests.rb" with: """ruby require 'vcr_config' VCR.use_cassette("example", :allow_unused_http_interactions => false) do Net::HTTP.get_response(URI("http://example.com/foo")) end """ When I run `ruby disallowed_with_all_requests.rb` Then it should pass Scenario: Does not silence other errors raised in `use_cassette` block Given a file named "does_not_silence_other_errors.rb" with: """ruby require 'vcr_config' VCR.use_cassette("example", :allow_unused_http_interactions => false) do raise "boom" end """ When I run `ruby does_not_silence_other_errors.rb` Then it should fail with "boom" And the output should not contain "There are unused HTTP interactions" vcr-5.0.0/features/cassettes/automatic_re_recording.feature000066400000000000000000000047271347305653000242270ustar00rootroot00000000000000Feature: Automatic Re-recording Over time, your cassettes may get out-of-date. APIs change and sites you scrape get updated. VCR provides a facility to automatically re-record your cassettes. Enable re-recording using the `:re_record_interval` option. The value provided should be an interval (expressed in seconds) that determines how often VCR will re-record the cassette. When a cassette is used, VCR checks the earliest `recorded_at` timestamp in the cassette; if more time than the interval has passed since that timestamp, VCR will use the `:all` record mode to cause it be re-recorded. Background: Given a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: get uri: http://localhost/ body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "12" body: encoding: UTF-8 string: Old Response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ And a file named "re_record.rb" with: """ruby $server = start_sinatra_app do get('/') { 'New Response' } end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' end VCR.use_cassette('example', :re_record_interval => 7.days, :match_requests_on => [:method, :host, :path]) do puts Net::HTTP.get_response('localhost', '/', $server.port).body end """ Scenario: Cassette is not re-recorded when not enough time has passed Given it is Tue, 07 Nov 2011 When I run `ruby re_record.rb` Then the output should contain "Old Response" But the output should not contain "New Response" And the file "cassettes/example.yml" should contain "Old Response" But the file "cassettes/example.yml" should not contain "New Response" Scenario: Cassette is re-recorded when enough time has passed Given it is Tue, 09 Nov 2011 When I run `ruby re_record.rb` Then the output should contain "New Response" But the output should not contain "Old Response" And the file "cassettes/example.yml" should contain "New Response" But the file "cassettes/example.yml" should not contain "Old Response" vcr-5.0.0/features/cassettes/decompress.feature000066400000000000000000000042141347305653000216520ustar00rootroot00000000000000Feature: Decode compressed response When the `:decode_compressed_response` option is set to a truthy value, VCR will decompress "gzip" and "deflate" response bodies before recording. This ensures that these interactions become readable and editable after being serialized. This option should be avoided if the actual decompression of response bodies is part of the functionality of the library or app being tested. Background: Given a file named "decompress.rb" with: """ruby require 'zlib' require 'stringio' $server = start_sinatra_app do get('/') { content = 'The quick brown fox jumps over the lazy dog' io = StringIO.new writer = Zlib::GzipWriter.new(io) writer << content writer.close headers['Content-Encoding'] = 'gzip' io.string } end require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' c.hook_into :webmock c.default_cassette_options = { :serialize_with => :syck } end """ Scenario: The option is not set by default When I append to file "decompress.rb": """ruby VCR.use_cassette(:decompress) do Net::HTTP.start('localhost', $server.port) do |http| http.get('/', 'accept-encoding' => 'identity') end end """ And I run `ruby decompress.rb` Then the file "cassettes/decompress.yml" should contain a YAML fragment like: """ Content-Encoding: - gzip """ Scenario: The option is enabled When I append to file "decompress.rb": """ruby VCR.use_cassette(:decompress, :decode_compressed_response => true) do Net::HTTP.start('localhost', $server.port) do |http| http.get('/', 'accept-encoding' => 'identity') end end """ And I run `ruby decompress.rb` Then the file "cassettes/decompress.yml" should contain a YAML fragment like: """ Content-Length: - '43' """ And the file "cassettes/decompress.yml" should contain: """ string: The quick brown fox jumps over the lazy dog """ vcr-5.0.0/features/cassettes/dynamic_erb.feature000066400000000000000000000057671347305653000220000ustar00rootroot00000000000000Feature: Dynamic ERB cassettes By default, cassettes are static: the exact response that was received when the cassette was recorded will be replayed for all future requests. Usually, this is fine, but in some cases you need something more dynamic. You can use ERB for this. To enable ERB evaluation of a cassette, pass the `:erb => true` option to a cassette. If you want to pass variables to the cassette, you can pass the names and values of the variables in a hash (`:erb => { ... }`). Scenario: Enable dynamic ERB cassette evalutation using :erb => true Given a previously recorded cassette file "cassettes/dynamic.yml" with: """ --- http_interactions: - request: method: get uri: http://example.com/foo?a=<%= 'b' * 3 %> body: encoding: UTF-8 string: '' headers: {} response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - '9' body: encoding: UTF-8 string: Hello <%= 'bar'.next %> http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ And a file named "dynamic_erb_example.rb" with: """ruby require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' end VCR.use_cassette('dynamic', :erb => true) do response = Net::HTTP.get_response('example.com', '/foo?a=bbb') puts "Response: #{response.body}" end """ When I run `ruby dynamic_erb_example.rb` Then it should pass with "Response: Hello bas" Scenario: Pass arguments to the ERB using :erb => { ... } Given a previously recorded cassette file "cassettes/dynamic.yml" with: """ --- http_interactions: - request: method: get uri: http://example.com/foo?a=<%= arg1 %> body: encoding: UTF-8 string: '' headers: {} response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - '9' body: encoding: UTF-8 string: Hello <%= arg2 %> http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ And a file named "dynamic_erb_example.rb" with: """ruby require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' end VCR.use_cassette('dynamic', :erb => { :arg1 => 7, :arg2 => 'baz' }) do response = Net::HTTP.get_response('example.com', '/foo?a=7') puts "Response: #{response.body}" end """ When I run `ruby dynamic_erb_example.rb` Then it should pass with "Response: Hello baz" vcr-5.0.0/features/cassettes/exclusive.feature000066400000000000000000000070121347305653000215140ustar00rootroot00000000000000Feature: exclusive cassette VCR allows cassettes to be nested. This is particularly useful in a context like cucumber, where you may be using a cassette for an entire scenario, and also using a cassette within a particular step definition. By default, both the inner and outer cassettes are active. On each request, VCR will look for a matching HTTP interaction in the inner cassette, and it will use the outer cassette as a fall back if none can be found. If you do not want the HTTP interactions of the outer cassette considered, you can pass the `:exclusive` option, so that the inner cassette is used exclusively. Background: Given a previously recorded cassette file "cassettes/outer.yml" with: """ --- http_interactions: - request: method: get uri: http://localhost/outer body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "18" body: encoding: UTF-8 string: Old outer response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ And a previously recorded cassette file "cassettes/inner.yml" with: """ --- http_interactions: - request: method: get uri: http://localhost/inner body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "18" body: encoding: UTF-8 string: Old inner response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ And a file named "setup.rb" with: """ruby include_http_adapter_for("net/http") $server = start_sinatra_app do get('/:path') { "New #{params[:path]} response" } end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.default_cassette_options = { :record => :new_episodes, :match_requests_on => [:method, :host, :path] } end """ Scenario: Cassettes are not exclusive by default Given a file named "not_exclusive.rb" with: """ruby require 'setup' VCR.use_cassette('outer') do VCR.use_cassette('inner') do puts response_body_for(:get, "http://localhost:#{$server.port}/outer") puts response_body_for(:get, "http://localhost:#{$server.port}/inner") end end """ When I run `ruby not_exclusive.rb` Then it should pass with: """ Old outer response Old inner response """ Scenario: Use an exclusive cassette Given a file named "exclusive.rb" with: """ruby require 'setup' VCR.use_cassette('outer') do VCR.use_cassette('inner', :exclusive => true) do puts response_body_for(:get, "http://localhost:#{$server.port}/outer") puts response_body_for(:get, "http://localhost:#{$server.port}/inner") end end """ When I run `ruby exclusive.rb` Then it should pass with: """ New outer response Old inner response """ And the file "cassettes/inner.yml" should contain "New outer response" vcr-5.0.0/features/cassettes/format.feature000066400000000000000000000315731347305653000210060ustar00rootroot00000000000000Feature: Cassette format VCR Cassettes are files that contain all of the information about the requests and corresponding responses in a human-readable/editable format. A cassette contains an array of HTTP interactions, each of which has the following: - request - method - uri - body - encoding - string - headers - response - status - code - message - headers - body - encoding - string - http version By default, VCR uses YAML to serialize this data. You can configure VCR to use a different serializer, either on a cassette-by-cassette basis, or as a default for all cassettes if you use the `default_cassette_options`. VCR supports the following serializers out of the box: - `:yaml`--Uses ruby's standard library YAML. This may use psych or syck, depending on your ruby installation. - `:syck`--Uses syck (the ruby 1.8 YAML engine). This is useful when using VCR on a project that must run in environments where psych is not available (such as on ruby 1.8), to ensure that syck is always used. - `:psych`--Uses psych (the new ruby 1.9 YAML engine). This is useful when you want to ensure that psych is always used. - `:json`--Uses [multi_json](https://github.com/intridea/multi_json) to serialize the cassette data as JSON. - `:compressed`--Wraps the default YAML serializer with Zlib, writing compressed cassettes to disk. You can also register a custom serializer using: VCR.configure do |config| config.cassette_serializers[:my_custom_serializer] = my_serializer end Your serializer must implement the following methods: - `file_extension` - `serialize(hash)` - `deserialize(string)` Scenario Outline: Request/Response data is saved to disk as YAML by default Given a file named "cassette_yaml.rb" with: """ruby include_http_adapter_for("") if ARGV.any? $server = start_sinatra_app do get('/:path') { ARGV[0] + ' ' + params[:path] } end end require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' c.before_record do |i| i.request.uri.sub!(/:\d+/, ':7777') end end VCR.use_cassette('example') do make_http_request(:get, "http://localhost:#{$server.port}/foo", nil, 'Accept-Encoding' => 'identity') make_http_request(:get, "http://localhost:#{$server.port}/bar", nil, 'Accept-Encoding' => 'identity') end """ When I successfully run `ruby cassette_yaml.rb 'Hello'` Then the file "cassettes/example.yml" should contain YAML like: """ --- http_interactions: - request: method: get uri: http://localhost:7777/foo body: encoding: UTF-8 string: "" headers: Accept-Encoding: - identity response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - "9" body: encoding: UTF-8 string: Hello foo http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: get uri: http://localhost:7777/bar body: encoding: UTF-8 string: "" headers: Accept-Encoding: - identity response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - "9" body: encoding: UTF-8 string: Hello bar http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :webmock | httpclient | | c.hook_into :webmock | patron | | c.hook_into :webmock | curb | | c.hook_into :webmock | em-http-request | | c.hook_into :webmock | typhoeus | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | Scenario: Request/Response data can be saved as JSON Given a file named "cassette_json.rb" with: """ruby include_http_adapter_for("net/http") $server = start_sinatra_app do get('/:path') { ARGV[0] + ' ' + params[:path] } end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.before_record do |i| i.request.uri.sub!(/:\d+/, ':7777') end c.default_cassette_options = { :match_requests_on => [:method, :host, :path] } end VCR.use_cassette('example', :serialize_with => :json) do puts response_body_for(:get, "http://localhost:#{$server.port}/foo", nil, 'Accept-Encoding' => 'identity') puts response_body_for(:get, "http://localhost:#{$server.port}/bar", nil, 'Accept-Encoding' => 'identity') end """ When I run `ruby cassette_json.rb 'Hello'` Then the file "cassettes/example.json" should contain JSON like: """json { "http_interactions": [ { "response": { "body": { "encoding": "UTF-8", "string": "Hello foo" }, "http_version": null, "status": { "code": 200, "message": "OK" }, "headers": { "Date": [ "Thu, 27 Oct 2011 06:16:31 GMT" ], "Content-Type": [ "text/html;charset=utf-8" ], "Content-Length": [ "9" ], "Server": [ "WEBrick/1.3.1 (Ruby/1.8.7/2011-06-30)" ], "Connection": [ "Keep-Alive" ] } }, "request": { "uri": "http://localhost:7777/foo", "body": { "encoding": "UTF-8", "string": "" }, "method": "get", "headers": { "Accept-Encoding": [ "identity" ] } }, "recorded_at": "Tue, 01 Nov 2011 04:58:44 GMT" }, { "response": { "body": { "encoding": "UTF-8", "string": "Hello bar" }, "http_version": null, "status": { "code": 200, "message": "OK" }, "headers": { "Date": [ "Thu, 27 Oct 2011 06:16:31 GMT" ], "Content-Type": [ "text/html;charset=utf-8" ], "Content-Length": [ "9" ], "Server": [ "WEBrick/1.3.1 (Ruby/1.8.7/2011-06-30)" ], "Connection": [ "Keep-Alive" ] } }, "request": { "uri": "http://localhost:7777/bar", "body": { "encoding": "UTF-8", "string": "" }, "method": "get", "headers": { "Accept-Encoding": [ "identity" ] } }, "recorded_at": "Tue, 01 Nov 2011 04:58:44 GMT" } ], "recorded_with": "VCR 2.0.0" } """ When I run `ruby cassette_json.rb` Then it should pass with: """ Hello foo Hello bar """ Scenario: Request/Response data can be saved as compressed YAML Given a file named "cassette_compressed.rb" with: """ruby include_http_adapter_for("net/http") $server = start_sinatra_app do get('/:path') { ARGV[0] + ' ' + params[:path] } end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.before_record do |i| i.request.uri.sub!(/:\d+/, ':7777') end c.default_cassette_options = { :match_requests_on => [:method, :host, :path] } end VCR.use_cassette('example', :serialize_with => :compressed) do puts response_body_for(:get, "http://localhost:#{$server.port}/foo", nil, 'Accept-Encoding' => 'identity') puts response_body_for(:get, "http://localhost:#{$server.port}/bar", nil, 'Accept-Encoding' => 'identity') end """ When I run `ruby cassette_compressed.rb 'Hello'` Then the file "cassettes/example.zz" should contain compressed YAML like: """ --- http_interactions: - request: method: get uri: http://localhost:7777/foo body: encoding: UTF-8 string: "" headers: Accept-Encoding: - identity response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - "9" body: encoding: UTF-8 string: Hello foo recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: get uri: http://localhost:7777/bar body: encoding: UTF-8 string: "" headers: Accept-Encoding: - identity response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - "9" body: encoding: UTF-8 string: Hello bar recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ When I run `ruby cassette_compressed.rb` Then it should pass with: """ Hello foo Hello bar """ Scenario: Request/Response data can be saved using a custom serializer Given a file named "cassette_ruby.rb" with: """ruby include_http_adapter_for("net/http") $server = start_sinatra_app do get('/:path') { ARGV[0] + ' ' + params[:path] } end require 'vcr' # purely for demonstration purposes; obviously, don't actually # use ruby #inspect / #eval for your serialization... ruby_serializer = Object.new class << ruby_serializer def file_extension; "ruby"; end def serialize(hash); hash.inspect; end def deserialize(string); eval(string); end end VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.cassette_serializers[:ruby] = ruby_serializer c.before_record do |i| i.request.uri.sub!(/:\d+/, ':7777') end c.default_cassette_options = { :match_requests_on => [:method, :host, :path] } end VCR.use_cassette('example', :serialize_with => :ruby) do puts response_body_for(:get, "http://localhost:#{$server.port}/foo", nil, 'Accept-Encoding' => 'identity') puts response_body_for(:get, "http://localhost:#{$server.port}/bar", nil, 'Accept-Encoding' => 'identity') end """ When I run `ruby cassette_ruby.rb 'Hello'` Then the file "cassettes/example.ruby" should contain ruby like: """ {"http_interactions"=> [{"request"=> {"method"=>"get", "uri"=>"http://localhost:7777/foo", "body"=>{"encoding"=>"UTF-8", "string"=>""}, "headers"=>{"Accept"=>["*/*"], "Accept-Encoding"=>["identity"], "User-Agent"=>["Ruby"]}}, "response"=> {"status"=>{"code"=>200, "message"=>"OK "}, "headers"=> {"Content-Type"=>["text/html;charset=utf-8"], "Content-Length"=>["9"], "Connection"=>["Keep-Alive"]}, "body"=>{"encoding"=>"UTF-8", "string"=>"Hello foo"}, "http_version"=>nil}, "recorded_at"=>"Tue, 01 Nov 2011 04:58:44 GMT"}, {"request"=> {"method"=>"get", "uri"=>"http://localhost:7777/bar", "body"=>{"encoding"=>"UTF-8", "string"=>""}, "headers"=>{"Accept"=>["*/*"], "Accept-Encoding"=>["identity"], "User-Agent"=>["Ruby"]}}, "response"=> {"status"=>{"code"=>200, "message"=>"OK "}, "headers"=> {"Content-Type"=>["text/html;charset=utf-8"], "Content-Length"=>["9"], "Connection"=>["Keep-Alive"]}, "body"=>{"encoding"=>"UTF-8", "string"=>"Hello bar"}, "http_version"=>nil}, "recorded_at"=>"Tue, 01 Nov 2011 04:58:44 GMT"}], "recorded_with"=>"VCR 2.0.0"} """ When I run `ruby cassette_ruby.rb` Then it should pass with: """ Hello foo Hello bar """ vcr-5.0.0/features/cassettes/freezing_time.feature000066400000000000000000000042341347305653000223370ustar00rootroot00000000000000Feature: Freezing Time When dealing with an HTTP API that includes time-based compontents in the request (e.g. for signed S3 requests), it can be useful on playback to freeze time to what it originally was when the cassette was recorded so that the request is always the same each time your test is run. While VCR doesn't directly support time freezing, it does expose `VCR::Cassette#originally_recorded_at`, which you can easily use with a library like [timecop](https://github.com/travisjeffery/timecop) to freeze time. Note: `VCR::Cassette#originally_recorded_at` will return `nil` when the cassette is recording for the first time, so you'll probably want to use an expression like `cassette.originally_recorded_at || Time.now` so that it will work when recording or when playing back. Scenario: Previously recorded responses are replayed Given a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: get uri: http://example.com/events/since/2013-09-23T17:00:30Z body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "20" body: encoding: UTF-8 string: Some Event http_version: "1.1" recorded_at: Mon, 23 Sep 2013 17:00:30 GMT recorded_with: VCR 2.0.0 """ Given a file named "freeze_time.rb" with: """ruby require 'time' require 'timecop' require 'vcr' VCR.configure do |vcr| vcr.cassette_library_dir = 'cassettes' vcr.hook_into :webmock end VCR.use_cassette('example') do |cassette| Timecop.freeze(cassette.originally_recorded_at || Time.now) do path = "/events/since/#{Time.now.getutc.iso8601}" response = Net::HTTP.get_response('example.com', path) puts "Response: #{response.body}" end end """ When I run `ruby freeze_time.rb` Then it should pass with "Response: Some Event" vcr-5.0.0/features/cassettes/naming.feature000066400000000000000000000016001347305653000207530ustar00rootroot00000000000000Feature: Naming When inserting or using a cassette, the first argument is the cassette name. You can use any string for the name. If you use the default `:file_system` storage backend, VCR will sanitize the string before using it as a file name, so that it is a file-system friendly file name. Scenario: Name sanitizing Given a file named "name_sanitizing.rb" with: """ruby $server = start_sinatra_app do get('/') { "Hello" } end require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' c.hook_into :webmock end VCR.use_cassette('Fee, Fi Fo Fum') do Net::HTTP.get_response('localhost', '/', $server.port) end """ And the directory "cassettes" does not exist When I run `ruby name_sanitizing.rb` Then the file "cassettes/Fee_Fi_Fo_Fum.yml" should contain "Hello" vcr-5.0.0/features/cassettes/no_cassette.feature000066400000000000000000000115111347305653000220130ustar00rootroot00000000000000Feature: Error for HTTP request made when no cassette is in use VCR is designed to help you remove all HTTP dependencies from your test suite. To assist with this, VCR will cause an exception to be raised when an HTTP request is made while there is no cassette in use. The error is helpful to pinpoint where HTTP requests are made so you can use a VCR cassette at that point in your code. If you want insight about how VCR attempted to handle the request, you can use the [debug\_logger](../configuration/debug-logging) configuration option to log more details. If you want to allow an HTTP request to proceed as normal, you can set the [allow\_http\_connections\_when\_no\_cassette](../configuration/allow-http-connections-when-no-cassette) configuration option or you can temporarily turn VCR off: - `VCR.turn_off!` => turn VCR off so HTTP requests are allowed. Cassette insertions will trigger an error. - `VCR.turn_off!(:ignore_cassettes => true)` => turn VCR off and ignore cassette insertions (so that no error is raised). - `VCR.turn_on!` => turn VCR back on - `VCR.turned_off { ... }` => turn VCR off for the duration of the provided block. @exclude-jruby Scenario Outline: Error for request when no cassette is in use Given a file named "no_cassette_error.rb" with: """ruby include_http_adapter_for("") require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' end response_body_for(:get, 'http://example.com/') """ When I run `ruby no_cassette_error.rb` Then it should fail with "An HTTP request has been made that VCR does not know how to handle" Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :webmock | httpclient | | c.hook_into :webmock | curb | | c.hook_into :webmock | patron | | c.hook_into :webmock | em-http-request | | c.hook_into :webmock | typhoeus | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | Scenario: Temporarily turn VCR off to allow HTTP requests to procede as normal Given a file named "turn_off_vcr.rb" with: """ruby $server = start_sinatra_app do get('/') { 'Hello' } end require 'vcr' VCR.configure do |c| c.hook_into :webmock end WebMock.allow_net_connect! def make_request(context) puts context puts Net::HTTP.get_response('localhost', '/', $server.port).body rescue => e puts "Error: #{e.class}" end VCR.turned_off do make_request "In VCR.turned_off block" end make_request "Outside of VCR.turned_off block" VCR.turn_off! make_request "After calling VCR.turn_off!" VCR.turn_on! make_request "After calling VCR.turn_on!" """ When I run `ruby turn_off_vcr.rb` Then the output should contain: """ In VCR.turned_off block Hello """ And the output should contain: """ Outside of VCR.turned_off block Error: VCR::Errors::UnhandledHTTPRequestError """ And the output should contain: """ After calling VCR.turn_off! Hello """ And the output should contain: """ After calling VCR.turn_on! Error: VCR::Errors::UnhandledHTTPRequestError """ Scenario: Turning VCR off prevents cassettes from being inserted Given a file named "turn_off_vcr_and_insert_cassette.rb" with: """ruby require 'vcr' VCR.configure do |c| c.hook_into :webmock end WebMock.allow_net_connect! VCR.turn_off! VCR.insert_cassette('example') """ When I run `ruby turn_off_vcr_and_insert_cassette.rb` Then it should fail with "VCR is turned off. You must turn it on before you can insert a cassette." Scenario: Turning VCR off with `:ignore_cassettes => true` ignores cassettes Given a file named "turn_off_vcr_and_insert_cassette.rb" with: """ruby $server = start_sinatra_app do get('/') { 'Hello' } end require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' c.hook_into :webmock end WebMock.allow_net_connect! VCR.turn_off!(:ignore_cassettes => true) VCR.use_cassette('example') do response = Net::HTTP.get_response('localhost', '/', $server.port).body puts "Response: #{response}" end """ When I run `ruby turn_off_vcr_and_insert_cassette.rb` Then it should pass with "Response: Hello" And the file "cassettes/example.yml" should not exist vcr-5.0.0/features/cassettes/update_content_length_header.feature000066400000000000000000000064171347305653000254020ustar00rootroot00000000000000Feature: Update content_length header When the `:update_content_length_header` option is set to a truthy value, VCR will ensure that the `Content-Length` header will have the correct value. This is useful in several situations: - When you manually edit the cassette file and change the resonse body length. You can use this option so you don't have to manually calculate and update the body length. - When you use ERB, the response body length may vary. This will ensure it is always correct. - Syck, the default YAML engine for ruby 1.8 (and 1.9, unless you compile it to use Psych), has a bug where it sometimes will remove some whitespace strings when you serialize them. This may cause the `Content-Length` header to have the wrong value. This is especially important when you use a client that checks the `Content-Length` header. Mechanize, for example, will raise an `EOFError` when the header value does not match the actual body length. Background: Given a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: get uri: http://example.com/ body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - "11" body: encoding: UTF-8 string: Hello http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ And a file named "common_stuff.rb" with: """ruby require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' c.hook_into :webmock end def make_request_and_print_results response = Net::HTTP.get_response('example.com', '/') puts "Body length: #{response.body.length}" puts "Header value: #{response['Content-Length']}" end """ Scenario: Default :update_content_length_header setting Given a file named "default.rb" with: """ruby require 'common_stuff' VCR.use_cassette('example') do make_request_and_print_results end """ When I run `ruby default.rb` Then the output should contain: """ Body length: 16 Header value: 11 """ Scenario: :update_content_length_header => false Given a file named "false.rb" with: """ruby require 'common_stuff' VCR.use_cassette('example', :update_content_length_header => false) do make_request_and_print_results end """ When I run `ruby false.rb` Then the output should contain: """ Body length: 16 Header value: 11 """ Scenario: :update_content_length_header => true Given a file named "true.rb" with: """ruby require 'common_stuff' VCR.use_cassette('example', :update_content_length_header => true) do make_request_and_print_results end """ When I run `ruby true.rb` Then the output should contain: """ Body length: 16 Header value: 16 """ vcr-5.0.0/features/configuration/000077500000000000000000000000001347305653000170015ustar00rootroot00000000000000vcr-5.0.0/features/configuration/allow_http_connections_when_no_cassette.feature000066400000000000000000000035301347305653000305460ustar00rootroot00000000000000Feature: Allow HTTP connections when no cassette Usually, HTTP requests made when no cassette is inserted will [result in an error](../cassettes/error-for-http-request-made-when-no-cassette-is-in-use). You can set the `allow_http_connections_when_no_cassette` configuration option to true to allow requests, if you do not want to use VCR for everything. Background: Given a file named "vcr_setup.rb" with: """ruby if ARGV.include?('--with-server') $server = start_sinatra_app do get('/') { "Hello" } end end require 'vcr' VCR.configure do |c| c.allow_http_connections_when_no_cassette = true c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.default_cassette_options = { :match_requests_on => [:method, :host, :path] } end """ And the directory "vcr/cassettes" does not exist Scenario: Allow HTTP connections when no cassette Given a file named "no_cassette.rb" with: """ruby require 'vcr_setup.rb' puts "Response: " + Net::HTTP.get_response('localhost', '/', $server ? $server.port : 0).body """ When I run `ruby no_cassette.rb --with-server` Then the output should contain "Response: Hello" Scenario: Cassettes record and replay as normal Given a file named "record_replay_cassette.rb" with: """ruby require 'vcr_setup.rb' VCR.use_cassette('localhost') do puts "Response: " + Net::HTTP.get_response('localhost', '/', $server ? $server.port : 0).body end """ When I run `ruby record_replay_cassette.rb --with-server` Then the output should contain "Response: Hello" And the file "cassettes/localhost.yml" should contain "Hello" When I run `ruby record_replay_cassette.rb` Then the output should contain "Response: Hello" vcr-5.0.0/features/configuration/cassette_library_dir.feature000066400000000000000000000017271347305653000245620ustar00rootroot00000000000000Feature: cassette_library_dir The `cassette_library_dir` configuration option sets a directory where VCR saves each cassette. Note: When using Rails, avoid using the `test/fixtures` directory to store the cassettes. Rails treats any YAML file in the fixtures directory as an ActiveRecord fixture. This will cause an `ActiveRecord::Fixture::FormatError` to be raised. Scenario: cassette_library_dir Given a file named "cassette_library_dir.rb" with: """ruby $server = start_sinatra_app do get('/') { "Hello" } end require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'vcr/cassettes' c.hook_into :webmock end VCR.use_cassette('localhost') do Net::HTTP.get_response('localhost', '/', $server.port) end """ And the directory "vcr/cassettes" does not exist When I run `ruby cassette_library_dir.rb` Then the file "vcr/cassettes/localhost.yml" should exist vcr-5.0.0/features/configuration/debug_logging.feature000066400000000000000000000061241347305653000231550ustar00rootroot00000000000000Feature: Debug Logging Use the `debug_logger` option to set an IO-like object that VCR will log debug output to. This is a useful way to troubleshoot what VCR is doing. The debug logger must respond to `#puts`. Scenario: Use the debug logger for troubleshooting Given a file named "debug_logger.rb" with: """ruby if ARGV.include?('--with-server') $server = start_sinatra_app do get('/') { "Hello World" } end end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.debug_logger = File.open(ARGV.first, 'w') c.default_cassette_options = { :match_requests_on => [:method, :host, :path] } end VCR.use_cassette('example') do port = $server ? $server.port : 7777 Net::HTTP.get_response(URI("http://localhost:#{port}/")) end """ When I run `ruby debug_logger.rb record.log --with-server` Given that port numbers in "record.log" are normalized to "7777" Then the file "record.log" should contain exactly: """ [Cassette: 'example'] Initialized with options: {:record=>:once, :match_requests_on=>[:method, :host, :path], :allow_unused_http_interactions=>true, :serialize_with=>:yaml, :persist_with=>:file_system} [webmock] Handling request: [get http://localhost:7777/] (disabled: false) [Cassette: 'example'] Initialized HTTPInteractionList with request matchers [:method, :host, :path] and 0 interaction(s): { } [webmock] Identified request type (recordable) for [get http://localhost:7777/] [Cassette: 'example'] Recorded HTTP interaction [get http://localhost:7777/] => [200 "Hello World"] """ When I run `ruby debug_logger.rb playback.log` Given that port numbers in "playback.log" are normalized to "7777" Then the file "playback.log" should contain exactly: """ [Cassette: 'example'] Initialized with options: {:record=>:once, :match_requests_on=>[:method, :host, :path], :allow_unused_http_interactions=>true, :serialize_with=>:yaml, :persist_with=>:file_system} [webmock] Handling request: [get http://localhost:7777/] (disabled: false) [Cassette: 'example'] Initialized HTTPInteractionList with request matchers [:method, :host, :path] and 1 interaction(s): { [get http://localhost:7777/] => [200 "Hello World"] } [Cassette: 'example'] Checking if [get http://localhost:7777/] matches [get http://localhost:7777/] using [:method, :host, :path] [Cassette: 'example'] method (matched): current request [get http://localhost:7777/] vs [get http://localhost:7777/] [Cassette: 'example'] host (matched): current request [get http://localhost:7777/] vs [get http://localhost:7777/] [Cassette: 'example'] path (matched): current request [get http://localhost:7777/] vs [get http://localhost:7777/] [Cassette: 'example'] Found matching interaction for [get http://localhost:7777/] at index 0: [200 "Hello World"] [webmock] Identified request type (stubbed_by_vcr) for [get http://localhost:7777/] """ vcr-5.0.0/features/configuration/default_cassette_options.feature000066400000000000000000000062371347305653000254600ustar00rootroot00000000000000Feature: default_cassette_options The `default_cassette_options` configuration option takes a hash that provides defaults for each cassette you use. Any cassette can override the defaults as well as set additional options. The `:match_requests_on` option defaults to `[:method, :uri]` when it has not been set. The `:record` option defaults to `:once` when it has not been set. Background: Given a file named "vcr_setup.rb" with: """ruby require 'vcr' VCR.configure do |c| c.default_cassette_options = { :record => :new_episodes, :erb => true } # not important for this example, but must be set to something c.hook_into :webmock c.cassette_library_dir = 'cassettes' end """ Scenario: cassettes get default values from configured `default_cassette_options` Given a file named "default_cassette_options.rb" with: """ruby require 'vcr_setup.rb' VCR.use_cassette('example') do puts "Record Mode: #{VCR.current_cassette.record_mode}" puts "ERB: #{VCR.current_cassette.erb}" end """ When I run `ruby default_cassette_options.rb` Then the output should contain: """ Record Mode: new_episodes ERB: true """ Scenario: `:match_requests_on` defaults to `[:method, :uri]` when it has not been set Given a file named "default_cassette_options.rb" with: """ruby require 'vcr_setup.rb' VCR.use_cassette('example') do puts "Match Requests On: #{VCR.current_cassette.match_requests_on.inspect}" end """ When I run `ruby default_cassette_options.rb` Then the output should contain "Match Requests On: [:method, :uri]" Scenario: `:record` defaults to `:once` when it has not been set Given a file named "default_record_mode.rb" with: """ruby require 'vcr' VCR.configure do |c| # not important for this example, but must be set to something c.hook_into :webmock c.cassette_library_dir = 'cassettes' end VCR.use_cassette('example') do puts "Record mode: #{VCR.current_cassette.record_mode.inspect}" end """ When I run `ruby default_record_mode.rb` Then the output should contain "Record mode: :once" Scenario: cassettes can set their own options Given a file named "default_cassette_options.rb" with: """ruby require 'vcr_setup.rb' VCR.use_cassette('example', :re_record_interval => 10000) do puts "Re-record Interval: #{VCR.current_cassette.re_record_interval}" end """ When I run `ruby default_cassette_options.rb` Then the output should contain "Re-record Interval: 10000" Scenario: cassettes can override default options Given a file named "default_cassette_options.rb" with: """ruby require 'vcr_setup.rb' VCR.use_cassette('example', :record => :none, :erb => false) do puts "Record Mode: #{VCR.current_cassette.record_mode}" puts "ERB: #{VCR.current_cassette.erb}" end """ When I run `ruby default_cassette_options.rb` Then the output should contain: """ Record Mode: none ERB: false """ vcr-5.0.0/features/configuration/filter_sensitive_data.feature000066400000000000000000000131061347305653000247260ustar00rootroot00000000000000Feature: Filter sensitive data Note: this config option is also available as `define_cassette_placeholder` to reflect the fact that it is useful for more than just sensitive data. The `filter_sensitive_data` configuration option can be used to prevent sensitive data from being written to your cassette files. This may be important if you commit your cassettes files to source control and do not want your sensitive data exposed. Pass the following arguments to `filter_sensitive_data`: - A substitution string. This is the string that will be written to the cassettte file as a placeholder. It should be unique and you may want to wrap it in special characters like `{ }` or `< >`. - A symbol specifying a tag (optional). If a tag is given, the filtering will only be applied to cassettes with the given tag. - A block. The block should return the sensitive text that you want replaced with the substitution string. If your block accepts an argument, the HTTP interaction will be yielded so that you can dynamically specify the sensitive text based on the interaction (see the last scenario for an example of this). When the interactions are replayed, the sensitive text will replace the substitution string so that the interaction will be identical to what was originally recorded. You can specify as many filterings as you want. Scenario: Multiple filterings Given a file named "filtering.rb" with: """ruby if ARGV.include?('--with-server') $server = start_sinatra_app do get('/') { "Hello World" } end end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.filter_sensitive_data('') { 'Hello' } c.filter_sensitive_data('') { 'World' } end VCR.use_cassette('filtering') do response = Net::HTTP.get_response('localhost', '/', $server ? $server.port : 0) puts "Response: #{response.body}" end """ When I run `ruby filtering.rb --with-server` Then the output should contain "Response: Hello World" And the file "cassettes/filtering.yml" should contain " " And the file "cassettes/filtering.yml" should not contain "Hello" And the file "cassettes/filtering.yml" should not contain "World" When I run `ruby filtering.rb` Then the output should contain "Hello World" Scenario: Filter tagged cassettes Given a file named "tagged_filtering.rb" with: """ruby if ARGV.include?('--with-server') response_count = 0 $server = start_sinatra_app do get('/') { "Hello World #{response_count += 1 }" } end end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.filter_sensitive_data('', :my_tag) { 'World' } end VCR.use_cassette('tagged', :tag => :my_tag) do response = Net::HTTP.get_response('localhost', '/', $server ? $server.port : 0) puts "Tagged Response: #{response.body}" end VCR.use_cassette('untagged', :record => :new_episodes) do response = Net::HTTP.get_response('localhost', '/', $server ? $server.port : 0) puts "Untagged Response: #{response.body}" end """ When I run `ruby tagged_filtering.rb --with-server` Then the output should contain each of the following: | Tagged Response: Hello World 1 | | Untagged Response: Hello World 2 | And the file "cassettes/tagged.yml" should contain "Hello 1" And the file "cassettes/untagged.yml" should contain "Hello World 2" When I run `ruby tagged_filtering.rb` Then the output should contain each of the following: | Tagged Response: Hello World 1 | | Untagged Response: Hello World 2 | Scenario: Filter dynamic data based on yielded HTTP interaction Given a file named "dynamic_filtering.rb" with: """ruby include_http_adapter_for('net/http') if ARGV.include?('--with-server') $server = start_sinatra_app do helpers do def request_header_for(header_key_fragment) key = env.keys.find { |k| k =~ /#{header_key_fragment}/i } env[key] end end get('/') { "#{request_header_for('username')}/#{request_header_for('password')}" } end end require 'vcr' USER_PASSWORDS = { 'john.doe' => 'monkey', 'jane.doe' => 'cheetah' } VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.filter_sensitive_data('') do |interaction| USER_PASSWORDS[interaction.request.headers['X-Http-Username'].first] end end VCR.use_cassette('example', :match_requests_on => [:method, :uri, :headers]) do port = $server ? $server.port : 0 puts "Response: " + response_body_for( :get, "http://localhost:#{port}/", nil, 'X-Http-Username' => 'john.doe', 'X-Http-Password' => USER_PASSWORDS['john.doe'] ) end """ When I run `ruby dynamic_filtering.rb --with-server` Then the output should contain "john.doe/monkey" And the file "cassettes/example.yml" should contain "john.doe/" And the file "cassettes/example.yml" should contain a YAML fragment like: """ X-Http-Password: - """ When I run `ruby dynamic_filtering.rb` Then the output should contain "john.doe/monkey" vcr-5.0.0/features/configuration/hook_into.feature000066400000000000000000000144441347305653000223560ustar00rootroot00000000000000Feature: hook_into The `hook_into` configuration option determines how VCR hooks into the HTTP requests to record and replay them. There are currently 4 valid options which support many different HTTP libraries: - :webmock can be used to hook into requests from: - Net::HTTP - HTTPClient - Patron - Curb (Curl::Easy, but not Curl::Multi) - EM HTTP Request - Typhoeus (Typhoeus::Hydra, but not Typhoeus::Easy or Typhoeus::Multi) - Excon - :typhoeus can be used to hook into itself (as long as you use Typhoeus::Hydra, but not Typhoeus::Easy or Typhoeus::Multi). - :excon can be used to hook into itself. - :faraday can be used to hook into itself. There are some addiitonal trade offs to consider when deciding which option to use: - WebMock uses extensive monkey patching to hook into supported HTTP libraries. No monkey patching is used for Typhoeus, Excon or Faraday. - Typhoeus, Excon, Faraday can be used together, and with WebMock. Regardless of which library you use, VCR takes care of all of the configuration for you. You should not need to interact directly with WebMock or the stubbing facilities of Typhoeus, Excon or Faraday. If/when you decide to change stubbing libraries, you can change the `hook_into` configuration option and it'll work with no other changes required. Scenario Outline: Record and replay a request using each supported hook_into/http library combination Given a file named "hook_into_http_lib_combo.rb" with: """ruby include_http_adapter_for("") require 'vcr' VCR.configure { |c| c.ignore_localhost = true } $server = start_sinatra_app do get('/') { ARGV[0] } end puts "The response for request 1 was: #{response_body_for(:get, "http://localhost:#{$server.port}/")}" VCR.configure do |c| c.cassette_library_dir = 'vcr_cassettes' c.ignore_localhost = false c.default_cassette_options = { :serialize_with => :syck } end VCR.use_cassette('example') do puts "The response for request 2 was: #{response_body_for(:get, "http://localhost:#{$server.port}/")}" end """ When I run `ruby hook_into_http_lib_combo.rb 'Hello World'` Then the output should contain each of the following: | The response for request 1 was: Hello World | | The response for request 2 was: Hello World | And the file "vcr_cassettes/example.yml" should contain "Hello World" When I run `ruby hook_into_http_lib_combo.rb 'Goodbye World'` Then the output should contain each of the following: | The response for request 1 was: Goodbye World | | The response for request 2 was: Hello World | And the file "vcr_cassettes/example.yml" should contain "Hello World" Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :webmock | httpclient | | c.hook_into :webmock | curb | | c.hook_into :webmock | patron | | c.hook_into :webmock | em-http-request | | c.hook_into :webmock | typhoeus | | c.hook_into :webmock | excon | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | | c.hook_into :faraday | faraday (w/ typhoeus) | @exclude-jruby @exclude-rbx Scenario Outline: Use Typhoeus, Excon and Faraday in combination with WebMock Given a file named "hook_into_multiple.rb" with: """ruby require 'typhoeus' require 'excon' require 'faraday' require 'vcr' VCR.configure { |c| c.ignore_localhost = true } $server = start_sinatra_app do get('/:path') { "#{ARGV[0]} #{params[:path]}" } end def net_http_response Net::HTTP.get_response('localhost', '/net_http', $server.port).body end def typhoeus_response Typhoeus::Request.get("http://localhost:#{$server.port}/typhoeus").body end def excon_response Excon.get("http://localhost:#{$server.port}/excon").body end def faraday_response Faraday::Connection.new(:url => "http://localhost:#{$server.port}") do |builder| builder.adapter : end.get('/faraday').body end puts "Net::HTTP 1: #{net_http_response}" puts "Typhoeus 1: #{typhoeus_response}" puts "Excon 1: #{excon_response}" puts "Faraday 1: #{faraday_response}" VCR.configure do |c| c.hook_into , :typhoeus, :excon, :faraday c.cassette_library_dir = 'vcr_cassettes' c.ignore_localhost = false end VCR.use_cassette('example') do puts "Net::HTTP 2: #{net_http_response}" puts "Typhoeus 2: #{typhoeus_response}" puts "Excon 2: #{excon_response}" puts "Faraday 2: #{faraday_response}" end """ When I run `ruby hook_into_multiple.rb 'Hello'` Then the output should contain each of the following: | Net::HTTP 1: Hello net_http | | Typhoeus 1: Hello typhoeus | | Excon 1: Hello excon | | Faraday 1: Hello faraday | | Net::HTTP 2: Hello net_http | | Typhoeus 2: Hello typhoeus | | Excon 2: Hello excon | | Faraday 2: Hello faraday | And the cassette "vcr_cassettes/example.yml" should have the following response bodies: | Hello net_http | | Hello typhoeus | | Hello excon | | Hello faraday | When I run `ruby hook_into_multiple.rb 'Goodbye'` Then the output should contain each of the following: | Net::HTTP 1: Goodbye net_http | | Typhoeus 1: Goodbye typhoeus | | Excon 1: Goodbye excon | | Faraday 1: Goodbye faraday | | Net::HTTP 2: Hello net_http | | Typhoeus 2: Hello typhoeus | | Excon 2: Hello excon | | Faraday 2: Hello faraday | Examples: | hook_into | faraday_adapter | extra_require | | :webmock | net_http | | | :webmock | typhoeus | require 'typhoeus/adapters/faraday' | vcr-5.0.0/features/configuration/ignore_request.feature000066400000000000000000000214531347305653000234160ustar00rootroot00000000000000Feature: Ignore Request By default, VCR hooks into every request, either allowing it and recording it, or playing back a recorded response, or raising an error to force you to deal with the new request. In some situations, you may prefer to have VCR ignore some requests. VCR provides 3 configuration options to accomplish this: * `ignore_request { |req| ... }` will ignore any request for which the given block returns true. * `ignore_hosts 'foo.com', 'bar.com'` allows you to specify particular hosts to ignore. * `ignore_localhost = true` is equivalent to `ignore_hosts 'localhost', '127.0.0.1', '0.0.0.0'`. It is particularly useful for when you use VCR with a javascript-enabled capybara driver, since capybara boots your rack app and makes localhost requests to it to check that it has booted. * `unignore_hosts 'foo.com', 'bar.com'` makes VCR stop ignoring particular hosts. Ignored requests are not recorded and are always allowed, regardless of the record mode, and even outside of a `VCR.use_cassette` block. Background: Given a file named "sinatra_app.rb" with: """ruby response_count = 0 $server = start_sinatra_app do get('/') { "Port 7777 Response #{response_count += 1}" } end """ @exclude-jruby Scenario Outline: ignore requests to a specific port Given a file named "ignore_request.rb" with: """ruby include_http_adapter_for("") require 'sinatra_app.rb' response_count = 0 $server_8888 = start_sinatra_app do get('/') { "Port 8888 Response #{response_count += 1}" } end require 'vcr' VCR.configure do |c| c.ignore_request do |request| URI(request.uri).port == $server.port end c.default_cassette_options = { :serialize_with => :syck } c.cassette_library_dir = 'cassettes' end VCR.use_cassette('example') do puts response_body_for(:get, "http://localhost:#{$server_8888.port}/") end VCR.use_cassette('example') do puts response_body_for(:get, "http://localhost:#{$server.port}/") end puts response_body_for(:get, "http://localhost:#{$server.port}/") puts response_body_for(:get, "http://localhost:#{$server_8888.port}/") """ When I run `ruby ignore_request.rb` Then it should fail with an error like: """ An HTTP request has been made that VCR does not know how to handle: """ And the output should contain: """ Port 8888 Response 1 Port 7777 Response 1 Port 7777 Response 2 """ And the file "cassettes/example.yml" should contain "Port 8888" And the file "cassettes/example.yml" should not contain "Port 7777" Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :typhoeus | typhoeus | | c.hook_into :faraday | faraday (w/ net_http) | Scenario Outline: ignored host requests are not recorded and are always allowed Given a file named "ignore_hosts.rb" with: """ruby include_http_adapter_for("") require 'sinatra_app.rb' require 'vcr' VCR.configure do |c| c.ignore_hosts '127.0.0.1', 'localhost' c.cassette_library_dir = 'cassettes' end VCR.use_cassette('example') do puts response_body_for(:get, "http://localhost:#{$server.port}/") end puts response_body_for(:get, "http://localhost:#{$server.port}/") """ When I run `ruby ignore_hosts.rb` Then it should pass with: """ Port 7777 Response 1 Port 7777 Response 2 """ And the file "cassettes/example.yml" should not exist Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | Scenario Outline: unignored host requests are recorded again Given a file named "unignore_hosts.rb" with: """ruby include_http_adapter_for("") require 'sinatra_app.rb' require 'vcr' VCR.configure do |c| c.ignore_hosts '127.0.0.1', 'localhost' c.cassette_library_dir = 'cassettes' end VCR.use_cassette('example') do puts response_body_for(:get, "http://localhost:#{$server.port}/") end VCR.configure do |c| c.unignore_hosts '127.0.0.1', 'localhost' end VCR.use_cassette('example') do puts response_body_for(:get, "http://localhost:#{$server.port}/") end """ When I run `ruby unignore_hosts.rb` Then it should pass with: """ Port 7777 Response 1 Port 7777 Response 2 """ And the file "cassettes/example.yml" should not contain "Response 1" And the file "cassettes/example.yml" should contain "Response 2" Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | Scenario Outline: unignored host requests are not allowed without a cassette Given a file named "unignore_hosts_without_cassette.rb" with: """ruby include_http_adapter_for("") require 'sinatra_app.rb' require 'vcr' VCR.configure do |c| c.ignore_hosts '127.0.0.1', 'localhost' c.cassette_library_dir = 'cassettes' end puts response_body_for(:get, "http://localhost:#{$server.port}/") VCR.configure do |c| c.unignore_hosts '127.0.0.1', 'localhost' end puts response_body_for(:get, "http://localhost:#{$server.port}/") """ When I run `ruby unignore_hosts_without_cassette.rb` Then it should fail with "An HTTP request has been made that VCR does not know how to handle" And the output should contain "Response 1" And the output should not contain "Response 2" And the file "cassettes/example.yml" should not exist Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | @exclude-jruby Scenario Outline: localhost requests are not treated differently by default Given a file named "localhost_not_ignored.rb" with: """ruby include_http_adapter_for("") require 'sinatra_app.rb' require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' c.default_cassette_options = { :serialize_with => :syck } end VCR.use_cassette('localhost') do response_body_for(:get, "http://localhost:#{$server.port}/") end response_body_for(:get, "http://localhost:#{$server.port}/") """ When I run `ruby localhost_not_ignored.rb` Then it should fail with "An HTTP request has been made that VCR does not know how to handle" And the file "cassettes/localhost.yml" should contain "Port 7777 Response 1" Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | Scenario Outline: localhost requests are allowed and not recorded when ignore_localhost = true Given a file named "ignore_localhost_true.rb" with: """ruby include_http_adapter_for("") require 'sinatra_app.rb' require 'vcr' VCR.configure do |c| c.ignore_localhost = true c.cassette_library_dir = 'cassettes' end VCR.use_cassette('localhost') do puts response_body_for(:get, "http://localhost:#{$server.port}/") end puts response_body_for(:get, "http://localhost:#{$server.port}/") """ When I run `ruby ignore_localhost_true.rb` Then it should pass with: """ Port 7777 Response 1 Port 7777 Response 2 """ And the file "cassettes/localhost.yml" should not exist Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | vcr-5.0.0/features/configuration/preserve_exact_body_bytes.feature000066400000000000000000000071001347305653000256160ustar00rootroot00000000000000@exclude-18 Feature: Preserve Exact Body Bytes Some HTTP servers are not well-behaved and respond with invalid data: the response body may not be encoded according to the encoding specified in the HTTP headers, or there may be bytes that are invalid for the given encoding. The YAML and JSON serializers are not generally designed to handle these cases gracefully, and you may get errors when the cassette is serialized or deserialized. Also, the encoding may not be preserved when round-tripped through the serializer. VCR provides a configuration option to deal with cases like these. The `preserve_exact_body_bytes` method accepts a block that VCR will use to determine if the body of the given request or response object should be base64 encoded in order to preserve the bytes exactly as-is. VCR does not do this by default, since base64-encoding the string removes the human readability of the cassette. Alternately, if you want to force an entire cassette to preserve the exact body bytes, you can pass the `:preserve_exact_body_bytes => true` cassette option when inserting your cassette. Scenario: Preserve exact bytes for response body with invalid encoding Given a file named "preserve.rb" with: """ruby # encoding: utf-8 string = "abc \xFA" puts "Valid encoding: #{string.valid_encoding?}" $server = start_sinatra_app do get('/') { string } end require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' c.hook_into :webmock c.preserve_exact_body_bytes do |http_message| http_message.body.encoding.name == 'ASCII-8BIT' || !http_message.body.valid_encoding? end end def make_request(label) puts puts label VCR.use_cassette('example', :serialize_with => :json) do body = Net::HTTP.get_response('localhost', '/', $server.port).body puts "Body: #{body.inspect}" end end make_request("Recording:") make_request("Playback:") """ When I run `ruby preserve.rb` Then the output should contain exactly: """ Valid encoding: false Recording: Body: "abc \xFA" Playback: Body: "abc \xFA" """ And the file "cassettes/example.json" should contain: """ "body":{"encoding":"ASCII-8BIT","base64_string":"YWJjIPo=\n"} """ Scenario: Preserve exact bytes for cassette with `:preserve_exact_body_bytes` option Given a file named "preserve.rb" with: """ruby $server = start_sinatra_app do get('/') { "Hello World" } end require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' c.hook_into :webmock c.default_cassette_options = { :serialize_with => :json } c.before_record do |i| # otherwise Ruby 2.0 will default to UTF-8: i.response.body.force_encoding('US-ASCII') end end VCR.use_cassette('preserve_bytes', :preserve_exact_body_bytes => true) do Net::HTTP.get_response('localhost', '/', $server.port) end VCR.use_cassette('dont_preserve_bytes') do Net::HTTP.get_response('localhost', '/', $server.port) end """ When I run `ruby preserve.rb` Then the file "cassettes/preserve_bytes.json" should contain: """ "body":{"encoding":"US-ASCII","base64_string":"SGVsbG8gV29ybGQ=\n"} """ And the file "cassettes/dont_preserve_bytes.json" should contain: """ "body":{"encoding":"US-ASCII","string":"Hello World"} """ vcr-5.0.0/features/configuration/query_parser.feature000066400000000000000000000047611347305653000231070ustar00rootroot00000000000000Feature: query_parser By default, VCR will parse query strings using `CGI.parse` from the Ruby standard library. This may not be the most optimal or performant library available. You can set the `query_parser` configuration option to use a different parser (such as `Rack::Utils.method(:parse_query)`) to decode, normalize, and/or provide a comparison object for query strings. The configured query parser needs to expose a `.call` method that returns an object which is comparable. This instance needs to implement the following API: * `#==` => boolean Background: Given a file named "cassettes/example.yml" with: """ --- http_interactions: - request: method: get uri: http://url.example.com/?bravo=2&alpha=1 body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "5" body: encoding: UTF-8 string: Hello http_version: "1.1" recorded_at: Tue, 25 Sep 2012 04:58:44 GMT recorded_with: VCR 2.2.5 """ Scenario: the VCR query parser gets its value from `query_parser` Given a file named "query_parser.rb" with: """ruby require 'vcr' require 'rack' VCR.configure do |c| c.query_parser = lambda { |query| raise query.inspect } c.default_cassette_options = {:match_requests_on => [:query]} c.hook_into :webmock c.cassette_library_dir = 'cassettes' end uri = URI.parse('http://other-url.example.com/?bravo=2&alpha=1') VCR.use_cassette('example') do puts Net::HTTP.get_response(uri).body end """ When I run `ruby query_parser.rb` Then it should fail with an error like: """ "alpha=1&bravo=2" """ Scenario: the `query_parser` defaults to the standard library's `CGI.parse` Given a file named "query_parser_default.rb" with: """ruby require 'vcr' VCR.configure do |c| c.hook_into :webmock c.default_cassette_options = {:match_requests_on => [:query]} c.cassette_library_dir = 'cassettes' end uri = URI.parse('http://other-url.example.com/?bravo=2&alpha=1') VCR.use_cassette('example') do puts Net::HTTP.get_response(uri).body end """ When I run `ruby query_parser_default.rb` Then it should pass with "Hello" vcr-5.0.0/features/configuration/uri_parser.feature000066400000000000000000000046661347305653000225450ustar00rootroot00000000000000Feature: uri_parser By default, VCR will parse URIs using `URI` from the Ruby standard library. There are some URIs seen out in the wild that `URI` cannot parse properly. You can set the `uri_parser` configuration option to use a different parser (such as `Addressable::URI`) to work with these URIs. The configured URI parser needs to expose a `.parse` class method that returns an instance of the uri. This uri instance needs to implement the following API: * `#scheme` => a string * `#host` => a string * `#port` => a fixnum * `#path` => a string * `#query` => a string * `#to_s` => a string * `#port=` * `#query=` * `#==` => boolean Background: Given a file named "cassettes/example.yml" with: """ --- http_interactions: - request: method: get uri: http://example.com/hello body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "5" body: encoding: UTF-8 string: Hello http_version: "1.1" recorded_at: Tue, 25 Sep 2012 04:58:44 GMT recorded_with: VCR 2.2.5 """ Scenario: the VCR uri parser gets its value from `uri_parser` Given a file named "uri_parser.rb" with: """ruby require 'vcr' require 'uri' module MyURI def self.parse(url) uri = URI.parse(url) uri.host = 'example.com' uri.path = '/hello' uri end end VCR.configure do |c| c.uri_parser = MyURI c.hook_into :webmock c.cassette_library_dir = 'cassettes' end VCR.use_cassette('example') do puts Net::HTTP.get_response('evil.org', '/projects').body end """ When I run `ruby uri_parser.rb` Then it should pass with "Hello" Scenario: the `uri_parser` defaults to the standard library's `URI` Given a file named "uri_parser_default.rb" with: """ruby require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' end VCR.use_cassette('example') do puts Net::HTTP.get_response('example.com', '/hello').body end """ When I run `ruby uri_parser_default.rb` Then it should pass with "Hello" vcr-5.0.0/features/getting_started.md000066400000000000000000000054031347305653000176450ustar00rootroot00000000000000### Install it [sudo] gem install vcr [sudo] gem install webmock ### Configure it Create a file named `vcr_setup.rb` with content like: require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'vcr_cassettes' c.hook_into :webmock end Ensure this file is required by your test suite before any of the tests are run. ### Use it Run your tests. Any tests that make HTTP requests using Net::HTTP will raise errors like: ================================================================================ An HTTP request has been made that VCR does not know how to handle: GET http://example.com/ There is currently no cassette in use. There are a few ways you can configure VCR to handle this request: * If you want VCR to record this request and play it back during future test runs, you should wrap your test (or this portion of your test) in a `VCR.use_cassette` block [1]. * If you only want VCR to handle requests made while a cassette is in use, configure `allow_http_connections_when_no_cassette = true`. VCR will ignore this request since it is made when there is no cassette [2]. * If you want VCR to ignore this request (and others like it), you can set an `ignore_request` callback [3]. [1] https://www.relishapp.com/myronmarston/vcr/v/2-0-0/docs/getting-started [2] https://www.relishapp.com/myronmarston/vcr/v/2-0-0/docs/configuration/allow-http-connections-when-no-cassette [3] https://www.relishapp.com/myronmarston/vcr/v/2-0-0/docs/configuration/ignore-request ================================================================================ Find one of these tests (preferably one that uses the same HTTP method and request URL every time--if not, you'll have to configure the request matcher). Wrap the body of it (or at least the code that makes the HTTP request) in a `VCR.use_cassette` block: VCR.use_cassette('whatever cassette name you want') do # the body of the test would go here... end Run this test. It will record the HTTP request to disk as a cassette (a test fixture), with content like: --- http_interactions: - request: method: get uri: http://example.com/ body: '' headers: {} response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - '26' body: This is the response body http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 Disconnect your computer from the internet. Run the test again. It should pass since VCR is automatically replaying the recorded response when the request is made. vcr-5.0.0/features/hooks/000077500000000000000000000000001347305653000152555ustar00rootroot00000000000000vcr-5.0.0/features/hooks/after_http_request.feature000066400000000000000000000043541347305653000225500ustar00rootroot00000000000000Feature: after_http_request hook The `after_http_request` hook gets called with each request and response just after a request has completed. It can be used for many things: * globally logging requests and responses * ejecting the current cassette (i.e. if you inserted it in a `before_http_request` hook) You can also pass one or more "filters" to `after_http_request`, to make the hook only be called for some requests. Any object that responds to `#to_proc` can be a filter. Here are some simple examples: * `:real?` -- only real requests * `:stubbed?` -- only stubbed requests * `:ignored?` -- only ignored requests * `:recordable?` -- only requests that are being recorded * `lambda { |req| URI(req.uri).host == 'amazon.com' }` -- only requests to amazon.com. Scenario Outline: log all requests and responses using after_http_request hook Given a file named "after_http_request.rb" with: """ruby include_http_adapter_for("") $server = start_sinatra_app do get('/foo') { "Hello World (foo)" } get('/bar') { "Hello World (bar)" } end require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' c.ignore_localhost = true c.after_http_request(:ignored?, lambda { |req| req.uri =~ /foo/ }) do |request, response| uri = request.uri.sub(/:\d+/, ":7777") puts "Response for #{request.method} #{uri}: #{response.body}" end end make_http_request(:get, "http://localhost:#{$server.port}/foo") make_http_request(:get, "http://localhost:#{$server.port}/bar") """ When I run `ruby after_http_request.rb` Then the output should contain "Response for get http://localhost:7777/foo: Hello World (foo)" But the output should not contain "bar" Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :webmock | httpclient | | c.hook_into :webmock | curb | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | vcr-5.0.0/features/hooks/around_http_request.feature000066400000000000000000000043411347305653000227330ustar00rootroot00000000000000@exclude-1.9.3p327 Feature: around_http_request hook The `around_http_request` hook wraps each HTTP request. It can be used rather than separate `before_http_request` and `after_http_request` hooks to simplify wrapping/transactional logic (such as using a VCR cassette). In your block, call `#proceed` on the yielded request to cause it to continue. Alternately, you can treat the request as a proc and pass it on to a method that expects a block by prefixing it with an ampersand (`&request`). Note that `around_http_request` will not work on Ruby 1.8. It uses a fiber under the covers and thus is only available on interpreters that support fibers. On 1.8, you can use separate `before_http_request` and `after_http_request` hooks. Scenario Outline: globally handle requests using an around_http_request hook Given a file named "globally_handle_requests.rb" with: """ruby include_http_adapter_for("") request_count = 0 $server = start_sinatra_app do get('/') { "Response #{request_count += 1 }" } end require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' c.default_cassette_options = { :serialize_with => :syck } c.around_http_request do |request| VCR.use_cassette('global', :record => :new_episodes, &request) end end puts "Response for request 1: " + response_body_for(:get, "http://localhost:#{$server.port}/") puts "Response for request 2: " + response_body_for(:get, "http://localhost:#{$server.port}/") """ When I run `ruby globally_handle_requests.rb` Then it should pass with: """ Response for request 1: Response 1 Response for request 2: Response 1 """ And the file "cassettes/global.yml" should contain "Response 1" Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :webmock | httpclient | | c.hook_into :webmock | curb | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | vcr-5.0.0/features/hooks/before_http_request.feature000066400000000000000000000044241347305653000227070ustar00rootroot00000000000000Feature: before_http_request hook The `before_http_request` hook gets called with each request just before it proceeds. It can be used for many things: * globally logging requests * inserting a particular cassette based on the request URI host * raising a timeout error You can also pass one or more "filters" to `before_http_request`, to make the hook only be called for some requests. Any object that responds to `#to_proc` can be a filter. Here are some simple examples: * `:real?` -- only real requests * `:stubbed?` -- only stubbed requests * `:ignored?` -- only ignored requests * `:recordable?` -- only requests that are being recorded * `lambda { |r| URI(r.uri).host == 'amazon.com' }` -- only requests to amazon.com. Scenario Outline: log all requests using a before_http_request hook Given a file named "before_http_request.rb" with: """ruby include_http_adapter_for("") if ARGV.include?('--with-server') $server = start_sinatra_app do get('/') { "Hello World" } end end require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' c.before_http_request(:real?) do |request| File.open(ARGV.first, 'w') do |f| f.write("before real request: #{request.method} #{request.uri}") end end end VCR.use_cassette('hook_example') do port = $server ? $server.port : 0 make_http_request(:get, "http://localhost:#{port}/") end """ When I run `ruby before_http_request.rb run1.log --with-server` Given that port numbers in "run1.log" are normalized to "7777" Then the file "run1.log" should contain "before real request: get http://localhost:7777/" When I run `ruby before_http_request.rb run2.log` Then the file "run2.log" should not exist Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :webmock | httpclient | | c.hook_into :webmock | curb | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | vcr-5.0.0/features/hooks/before_playback.feature000066400000000000000000000125351347305653000217500ustar00rootroot00000000000000Feature: before_playback hook The `before_playback` hook is called before a cassette sets up its stubs for playback. Your block should accept up to 2 arguments. The first argument will be the HTTP interaction that is about to be used for play back. The second argument will be the current cassette. You can also call `#ignore!` on the HTTP interaction to prevent VCR from playing it back. You can use tags to specify a cassette, otherwise your hook will apply to all cassettes. Consider this code: VCR.configure do |c| c.before_playback(:twitter) { ... } # modify the interactions somehow end VCR.use_cassette('cassette_1', :tag => :twitter) { ... } VCR.use_cassette('cassette_2') { ... } In this example, the hook would apply to the first cassette but not the second cassette. Background: Given a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: get uri: http://localhost:7777/ body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - "20" body: encoding: UTF-8 string: previously recorded response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Scenario: Modify played back response Given a file named "before_playback_example.rb" with: """ruby require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.before_playback do |interaction| interaction.response.body = 'response from before_playback' end end VCR.use_cassette('example') do response = Net::HTTP.get_response('localhost', '/', 7777) puts "Response: #{response.body}" end """ When I run `ruby before_playback_example.rb` Then it should pass with "Response: response from before_playback" Scenario: Modify played back response based on the cassette Given a file named "before_playback_example.rb" with: """ruby require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.before_playback do |interaction, cassette| interaction.response.body = "response for #{cassette.name} cassette" end end VCR.use_cassette('example') do response = Net::HTTP.get_response('localhost', '/', 7777) puts "Response: #{response.body}" end """ When I run `ruby before_playback_example.rb` Then it should pass with "Response: response for example cassette" Scenario: Prevent playback by ignoring interaction in before_playback hook Given a file named "before_playback_ignore.rb" with: """ruby $server = start_sinatra_app do get('/') { "sinatra response" } end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.before_playback { |i| i.ignore! } end VCR.use_cassette('localhost', :record => :new_episodes, :match_requests_on => [:method, :host, :path]) do response = Net::HTTP.get_response('localhost', '/', $server.port) puts "Response: #{response.body}" end """ When I run `ruby before_playback_ignore.rb` Then it should pass with "Response: sinatra response" Scenario: Multiple hooks are run in order Given a file named "multiple_hooks.rb" with: """ruby require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.before_playback { puts "In before_playback hook 1" } c.before_playback { puts "In before_playback hook 2" } end VCR.use_cassette('example', :record => :new_episodes) do response = Net::HTTP.get_response('localhost', '/', 7777) puts "Response: #{response.body}" end """ When I run `ruby multiple_hooks.rb` Then it should pass with: """ In before_playback hook 1 In before_playback hook 2 Response: previously recorded response """ Scenario: Use tagging to apply hooks to only certain cassettes Given a file named "tagged_hooks.rb" with: """ruby require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.before_playback(:tag_2) do |i| puts "In before_playback hook for tag_2" end end [:tag_1, :tag_2, nil].each do |tag| puts puts "Using tag: #{tag.inspect}" VCR.use_cassette('example', :record => :new_episodes, :tag => tag) do response = Net::HTTP.get_response('localhost', '/', 7777) puts "Response: #{response.body}" end end """ When I run `ruby tagged_hooks.rb` Then it should pass with: """ Using tag: :tag_1 Response: previously recorded response Using tag: :tag_2 In before_playback hook for tag_2 Response: previously recorded response Using tag: nil Response: previously recorded response """ vcr-5.0.0/features/hooks/before_record.feature000066400000000000000000000117141347305653000214360ustar00rootroot00000000000000Feature: before_record hook The `before_record` hook is called before a cassette is written to disk. This can be used to modify the HTTP interaction before it is recorded. Your block should accept up to 2 arguments. The first argument will be the HTTP interaction that is about to be written to disk. The second argument will be the current cassette. If you wish to prevent VCR from recording the HTTP interaction you can call `#ignore!` on the interaction. If you don't want your hook to apply to all cassettes, you can use tags to select which cassettes a given hook applies to. Consider this code: VCR.configure do |c| c.before_record(:twitter) { ... } # modify the interactions somehow end VCR.use_cassette('cassette_1', :tag => :twitter) { ... } VCR.use_cassette('cassette_2') { ... } In this example, the hook would apply to the first cassette but not the second cassette. Scenario: Modify recorded response Given a file named "before_record_example.rb" with: """ruby $server = start_sinatra_app do get('/') { "Hello Earth" } end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.before_record do |i| i.response.body.sub!('Earth', 'World') end end VCR.use_cassette('recording_example') do Net::HTTP.get_response('localhost', '/', $server.port) end """ When I run `ruby before_record_example.rb` Then the file "cassettes/recording_example.yml" should contain "Hello World" And the file "cassettes/recording_example.yml" should not contain "Earth" Scenario: Modify recorded response based on the cassette Given a file named "before_record_example.rb" with: """ruby $server = start_sinatra_app do get('/') { "Hello Earth" } end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.before_record do |interaction, cassette| interaction.response.body << " (#{cassette.name})" end end VCR.use_cassette('recording_example') do Net::HTTP.get_response('localhost', '/', $server.port) end """ When I run `ruby before_record_example.rb` Then the file "cassettes/recording_example.yml" should contain "Hello Earth (recording_example)" Scenario: Prevent recording by ignoring interaction in before_record hook Given a file named "before_record_ignore.rb" with: """ruby $server = start_sinatra_app do get('/') { "Hello World" } end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.before_record { |i| i.ignore! } end VCR.use_cassette('recording_example') do response = Net::HTTP.get_response('localhost', '/', $server.port) puts "Response: #{response.body}" end """ When I run `ruby before_record_ignore.rb` Then it should pass with "Response: Hello World" And the file "cassettes/recording_example.yml" should not exist Scenario: Multiple hooks are run in order Given a file named "multiple_hooks.rb" with: """ruby $server = start_sinatra_app do get('/') { "Hello World" } end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.before_record { puts "In before_record hook 1" } c.before_record { puts "In before_record hook 2" } end VCR.use_cassette('example', :record => :new_episodes) do response = Net::HTTP.get_response('localhost', '/', $server.port) puts "Response: #{response.body}" end """ When I run `ruby multiple_hooks.rb` Then it should pass with: """ Response: Hello World In before_record hook 1 In before_record hook 2 """ Scenario: Use tagging to apply hook to only certain cassettes Given a file named "tagged_hooks.rb" with: """ruby $server = start_sinatra_app do get('/') { "Hello World" } end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.before_record(:tag_1) do puts "In before_record hook for tag_1" end end [:tag_1, :tag_2, nil].each do |tag| puts puts "Using tag: #{tag.inspect}" VCR.use_cassette('example', :record => :new_episodes, :tag => tag) do response = Net::HTTP.get_response('localhost', '/', $server.port) puts "Response: #{response.body}" end end """ When I run `ruby tagged_hooks.rb` Then it should pass with: """ Using tag: :tag_1 Response: Hello World In before_record hook for tag_1 Using tag: :tag_2 Response: Hello World Using tag: nil Response: Hello World """ vcr-5.0.0/features/http_libraries/000077500000000000000000000000001347305653000171455ustar00rootroot00000000000000vcr-5.0.0/features/http_libraries/em_http_request.feature000066400000000000000000000142711347305653000237370ustar00rootroot00000000000000@exclude-jruby @exclude-rbx Feature: EM HTTP Request EM HTTP Request allows multiple simultaneous asynchronous requests. (The other HTTP libraries are synchronous). The scenarios below demonstrate how VCR can be used with asynchronous em-http requests. Background: Given a file named "vcr_setup.rb" with: """ruby require 'em-http-request' $server = start_sinatra_app do %w[ foo bar bazz ].each_with_index do |path, index| get "/#{path}" do sleep index * 0.1 # ensure the async callbacks are invoked in order ARGV[0] + ' ' + path end end end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.before_record do |i| i.request.uri.sub!(/:\d+/, ':7777') end end """ Scenario: multiple simultaneous HttpRequest objects Given a file named "make_requests.rb" with: """ruby require 'vcr_setup' VCR.use_cassette('em_http') do EventMachine.run do http_array = %w[ foo bar bazz ].map do |p| EventMachine::HttpRequest.new("http://localhost:#{$server.port}/#{p}").get end http_array.each do |http| http.callback do puts http.response if http_array.all? { |h| h.response.to_s != '' } EventMachine.stop end end end end end """ When I run `ruby make_requests.rb Hello` Then the output should contain: """ Hello foo Hello bar Hello bazz """ And the file "cassettes/em_http.yml" should contain YAML like: """ --- http_interactions: - request: method: get uri: http://localhost:7777/foo body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - "9" body: encoding: UTF-8 string: Hello foo http_version: recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: get uri: http://localhost:7777/bar body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - "9" body: encoding: UTF-8 string: Hello bar http_version: recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: get uri: http://localhost:7777/bazz body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - "10" body: encoding: UTF-8 string: Hello bazz http_version: recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ When I run `ruby make_requests.rb Goodbye` Then the output should contain: """ Hello foo Hello bar Hello bazz """ Scenario: MultiRequest Given a file named "make_requests.rb" with: """ruby require 'vcr_setup' VCR.use_cassette('em_http') do EventMachine.run do multi = EventMachine::MultiRequest.new %w[ foo bar bazz ].each do |path| multi.add(path, EventMachine::HttpRequest.new("http://localhost:#{$server.port}/#{path}").get) end multi.callback do responses = Hash[multi.responses[:callback]] %w[ foo bar bazz ].each do |path| puts responses[path].response end EventMachine.stop end end end """ When I run `ruby make_requests.rb Hello` Then the output should contain: """ Hello foo Hello bar Hello bazz """ And the file "cassettes/em_http.yml" should contain YAML like: """ --- http_interactions: - request: method: get uri: http://localhost:7777/foo body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - "9" body: encoding: UTF-8 string: Hello foo http_version: recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: get uri: http://localhost:7777/bar body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - "9" body: encoding: UTF-8 string: Hello bar http_version: recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: get uri: http://localhost:7777/bazz body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - "10" body: encoding: UTF-8 string: Hello bazz http_version: recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ When I run `ruby make_requests.rb Goodbye` Then the output should contain: """ Hello foo Hello bar Hello bazz """ vcr-5.0.0/features/http_libraries/net_http.feature000066400000000000000000000121551347305653000223530ustar00rootroot00000000000000Feature: Net::HTTP There are many ways to use Net::HTTP. The scenarios below provide regression tests for some Net::HTTP APIs that have not worked properly with VCR and WebMock in the past (but have since been fixed). Background: Given a file named "vcr_setup.rb" with: """ruby require 'ostruct' if ARGV[0] == '--with-server' $server = start_sinatra_app do get('/') { 'VCR works with Net::HTTP gets!' } post('/') { 'VCR works with Net::HTTP posts!' } end else $server = OpenStruct(:port => 0) end require 'vcr' VCR.configure do |c| c.default_cassette_options = { :match_requests_on => [:method, :host, :path] } end """ Scenario Outline: Calling #post on new Net::HTTP instance Given a file named "vcr_net_http.rb" with: """ruby require 'vcr_setup.rb' VCR.configure do |c| c.hook_into c.cassette_library_dir = 'cassettes' end VCR.use_cassette('net_http') do puts Net::HTTP.new('localhost', $server.port).post('/', '').body end """ When I run `ruby vcr_net_http.rb --with-server` Then the output should contain "VCR works with Net::HTTP posts!" And the file "cassettes/net_http.yml" should contain "VCR works with Net::HTTP posts!" When I run `ruby vcr_net_http.rb` Then the output should contain "VCR works with Net::HTTP posts!" Examples: | hook_into | | :webmock | Scenario Outline: Return from yielded block Given a file named "vcr_net_http.rb" with: """ruby require 'vcr_setup.rb' VCR.configure do |c| c.hook_into c.cassette_library_dir = 'cassettes' end def perform_request Net::HTTP.new('localhost', $server.port).request(Net::HTTP::Get.new('/', {})) do |response| return response end end VCR.use_cassette('net_http') do puts perform_request.body end """ When I run `ruby vcr_net_http.rb --with-server` Then the output should contain "VCR works with Net::HTTP gets!" And the file "cassettes/net_http.yml" should contain "VCR works with Net::HTTP gets!" When I run `ruby vcr_net_http.rb` Then the output should contain "VCR works with Net::HTTP gets!" Examples: | hook_into | | :webmock | Scenario Outline: Use Net::ReadAdapter to read body in fragments Given a file named "vcr_net_http.rb" with: """ruby require 'vcr_setup.rb' VCR.configure do |c| c.hook_into c.cassette_library_dir = 'cassettes' end VCR.use_cassette('net_http') do body = '' Net::HTTP.new('localhost', $server.port).request_get('/') do |response| response.read_body { |frag| body << frag } end puts body end """ When I run `ruby vcr_net_http.rb --with-server` Then the output should contain "VCR works with Net::HTTP gets!" And the file "cassettes/net_http.yml" should contain "VCR works with Net::HTTP gets!" When I run `ruby vcr_net_http.rb` Then the output should contain "VCR works with Net::HTTP gets!" Examples: | hook_into | | :webmock | Scenario Outline: Use open-uri (which is built on top of Net::HTTP and uses a seldom-used Net::HTTP API) Given a file named "vcr_net_http.rb" with: """ruby require 'open-uri' require 'vcr_setup.rb' VCR.configure do |c| c.hook_into c.cassette_library_dir = 'cassettes' end VCR.use_cassette('net_http') do puts open("http://localhost:#{$server.port}/").read end """ When I run `ruby vcr_net_http.rb --with-server` Then the output should contain "VCR works with Net::HTTP gets!" And the file "cassettes/net_http.yml" should contain "VCR works with Net::HTTP gets!" When I run `ruby vcr_net_http.rb` Then the output should contain "VCR works with Net::HTTP gets!" Examples: | hook_into | | :webmock | Scenario Outline: Make an HTTPS request Given a file named "vcr_https.rb" with: """ruby require 'vcr' VCR.configure do |c| c.hook_into c.cassette_library_dir = 'cassettes' end uri = URI("https://gist.githubusercontent.com/myronmarston/fb555cb593f3349d53af/raw/6921dd638337d3f6a51b0e02e7f30e3c414f70d6/vcr_gist") VCR.use_cassette('https') do http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE response = http.request_get(uri.path) puts response.body end """ When I run `ruby vcr_https.rb` Then the output should contain "VCR gist" And the file "cassettes/https.yml" should contain "VCR gist" When I modify the file "cassettes/https.yml" to replace "VCR gist" with "HTTPS replaying works" And I run `ruby vcr_https.rb` Then the output should contain "HTTPS replaying works" Examples: | hook_into | | :webmock | vcr-5.0.0/features/middleware/000077500000000000000000000000001347305653000162475ustar00rootroot00000000000000vcr-5.0.0/features/middleware/faraday.feature000066400000000000000000000034361347305653000212410ustar00rootroot00000000000000Feature: Faraday middleware VCR provides middleware that can be used with Faraday. You can use this as an alternative to Faraday's built-in test adapter. VCR will automatically insert this middleware in the Faraday stack when you configure `hook_into :faraday`. However, if you want to control where the middleware goes in the faraday stack, you can use it yourself. The middleware should come before the Faraday HTTP adapter. Note that when you use the middleware directly, you don't need to configure `hook_into :faraday`. Scenario Outline: Use Faraday middleware Given a file named "faraday_example.rb" with: """ruby request_count = 0 $server = start_sinatra_app do get('/:path') { "Hello #{params[:path]} #{request_count += 1}" } end require 'faraday' require 'vcr' VCR.configure do |c| c.default_cassette_options = { :serialize_with => :syck } c.cassette_library_dir = 'cassettes' end conn = Faraday::Connection.new(:url => "http://localhost:#{$server.port}") do |builder| builder.use VCR::Middleware::Faraday builder.adapter : end VCR.use_cassette('example') do puts "Response 1: #{conn.get('/foo').body}" end VCR.use_cassette('example') do puts "Response 2: #{conn.get('/foo').body}" end """ When I run `ruby faraday_example.rb` Then the output should contain: """ Response 1: Hello foo 1 Response 2: Hello foo 1 """ And the file "cassettes/example.yml" should contain "Hello foo 1" Examples: | adapter | extra_require | | net_http | | | typhoeus | require 'typhoeus/adapters/faraday' | vcr-5.0.0/features/middleware/rack.feature000066400000000000000000000056601347305653000205530ustar00rootroot00000000000000Feature: Rack VCR provides a rack middleware that uses a cassette for the duration of a request. Simply provide `VCR::Middleware::Rack` with a block that sets the cassette name and options. You can set these based on the rack env if your block accepts two arguments. This is useful in a couple different ways: - In a rails app, you could use this to log all HTTP API calls made by the rails app (using the `:all` record mode). Of course, this will only record HTTP API calls made in the request-response cycle--API calls that are offloaded to a background job will not be logged. - This can be used as middleware in a simple rack HTTP proxy, to record and replay the proxied requests. Background: Given a file named "remote_server.rb" with: """ruby request_count = 0 $server = start_sinatra_app do get('/:path') { "Hello #{params[:path]} #{request_count += 1}" } end """ And a file named "client.rb" with: """ruby require 'remote_server' require 'proxy_server' require 'cgi' url = URI.parse("http://localhost:#{$proxy.port}?url=#{CGI.escape("http://localhost:#{$server.port}/foo")}") puts "Response 1: #{Net::HTTP.get_response(url).body}" puts "Response 2: #{Net::HTTP.get_response(url).body}" """ And the directory "cassettes" does not exist Scenario: Use VCR rack middleware to record HTTP responses for a simple rack proxy app Given a file named "proxy_server.rb" with: """ruby require 'vcr' $proxy = start_sinatra_app do use VCR::Middleware::Rack do |cassette| cassette.name 'proxied' cassette.options :record => :new_episodes end get('/') { Net::HTTP.get_response(URI.parse(params[:url])).body } end VCR.configure do |c| c.cassette_library_dir = 'cassettes' c.hook_into :webmock c.allow_http_connections_when_no_cassette = true end """ When I run `ruby client.rb` Then the output should contain: """ Response 1: Hello foo 1 Response 2: Hello foo 1 """ And the file "cassettes/proxied.yml" should contain "Hello foo 1" Scenario: Set cassette name based on rack request env Given a file named "proxy_server.rb" with: """ruby require 'vcr' $proxy = start_sinatra_app do use VCR::Middleware::Rack do |cassette, env| cassette.name env['SERVER_NAME'] end get('/') { Net::HTTP.get_response(URI.parse(params[:url])).body } end VCR.configure do |c| c.cassette_library_dir = 'cassettes' c.hook_into :webmock c.allow_http_connections_when_no_cassette = true end """ When I run `ruby client.rb` Then the output should contain: """ Response 1: Hello foo 1 Response 2: Hello foo 1 """ And the file "cassettes/localhost.yml" should contain "Hello foo 1" vcr-5.0.0/features/record_modes/000077500000000000000000000000001347305653000165775ustar00rootroot00000000000000vcr-5.0.0/features/record_modes/all.feature000066400000000000000000000045351347305653000207330ustar00rootroot00000000000000Feature: :all The `:all` record mode will: - Record new interactions. - Never replay previously recorded interactions. This can be temporarily used to force VCR to re-record a cassette (i.e. to ensure the responses are not out of date) or can be used when you simply want to log all HTTP requests. Background: Given a file named "setup.rb" with: """ruby $server = start_sinatra_app do get('/') { 'Hello' } get('/foo') { 'Goodbye' } end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' end """ And a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: get uri: http://localhost/ body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "20" body: encoding: UTF-8 string: old response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Scenario: Re-record previously recorded response Given a file named "re_record.rb" with: """ruby require 'setup' VCR.use_cassette('example', :record => :all, :match_requests_on => [:method, :host, :path]) do response = Net::HTTP.get_response('localhost', '/', $server.port) puts "Response: #{response.body}" end """ When I run `ruby re_record.rb` Then it should pass with "Response: Hello" And the file "cassettes/example.yml" should contain "Hello" But the file "cassettes/example.yml" should not contain "old response" Scenario: Record new request Given a file named "record_new.rb" with: """ruby require 'setup' VCR.use_cassette('example', :record => :all) do response = Net::HTTP.get_response('localhost', '/foo', $server.port) puts "Response: #{response.body}" end """ When I run `ruby record_new.rb` Then it should pass with "Response: Goodbye" And the file "cassettes/example.yml" should contain each of these: | old response | | Goodbye | vcr-5.0.0/features/record_modes/new_episodes.feature000066400000000000000000000044171347305653000226460ustar00rootroot00000000000000Feature: :new_episodes The `:new_episodes` record mode will: - Record new interactions. - Replay previously recorded interactions. It is similar to the `:once` record mode, but will _always_ record new interactions, even if you have an existing recorded one that is similar (but not identical, based on the `:match_request_on` option). Background: Given a file named "setup.rb" with: """ruby $server = start_sinatra_app do get('/') { 'Hello' } end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' end """ And a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: get uri: http://example.com/foo body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "20" body: encoding: UTF-8 string: example.com response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Scenario: Previously recorded responses are replayed Given a file named "replay_recorded_response.rb" with: """ruby require 'setup' VCR.use_cassette('example', :record => :new_episodes) do response = Net::HTTP.get_response('example.com', '/foo') puts "Response: #{response.body}" end """ When I run `ruby replay_recorded_response.rb` Then it should pass with "Response: example.com response" Scenario: New requests get recorded Given a file named "record_new_requests.rb" with: """ruby require 'setup' VCR.use_cassette('example', :record => :new_episodes) do response = Net::HTTP.get_response('localhost', '/', $server.port) puts "Response: #{response.body}" end """ When I run `ruby record_new_requests.rb` Then it should pass with "Response: Hello" And the file "cassettes/example.yml" should contain each of these: | example.com response | | Hello | vcr-5.0.0/features/record_modes/none.feature000066400000000000000000000037651347305653000211260ustar00rootroot00000000000000Feature: :none The `:none` record mode will: - Replay previously recorded interactions. - Cause an error to be raised for any new requests. This is useful when your code makes potentially dangerous HTTP requests. The `:none` record mode guarantees that no new HTTP requests will be made. Background: Given a file named "vcr_config.rb" with: """ruby require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' end """ And a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: get uri: http://example.com/foo body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "5" body: encoding: UTF-8 string: Hello http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Scenario: Previously recorded responses are replayed Given a file named "replay_recorded_response.rb" with: """ruby require 'vcr_config' VCR.use_cassette('example', :record => :none) do response = Net::HTTP.get_response('example.com', '/foo') puts "Response: #{response.body}" end """ When I run `ruby replay_recorded_response.rb` Then it should pass with "Response: Hello" @exclude-jruby Scenario: New requests are prevented Given a file named "prevent_new_request.rb" with: """ruby require 'vcr_config' VCR.use_cassette('example', :record => :none) do Net::HTTP.get_response('example.com', '/bar') end """ When I run `ruby prevent_new_request.rb` Then it should fail with "An HTTP request has been made that VCR does not know how to handle" vcr-5.0.0/features/record_modes/once.feature000066400000000000000000000056651347305653000211140ustar00rootroot00000000000000Feature: :once The `:once` record mode will: - Replay previously recorded interactions. - Record new interactions if there is no cassette file. - Cause an error to be raised for new requests if there is a cassette file. It is similar to the `:new_episodes` record mode, but will prevent new, unexpected requests from being made (i.e. because the request URI changed or whatever). `:once` is the default record mode, used when you do not set one. Background: Given a file named "setup.rb" with: """ruby $server = start_sinatra_app do get('/') { 'Hello' } end require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' end """ And a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: get uri: http://example.com/foo body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "20" body: encoding: UTF-8 string: example.com response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Scenario: Previously recorded responses are replayed Given a file named "replay_recorded_response.rb" with: """ruby require 'setup' VCR.use_cassette('example', :record => :once) do response = Net::HTTP.get_response('example.com', '/foo') puts "Response: #{response.body}" end """ When I run `ruby replay_recorded_response.rb` Then it should pass with "Response: example.com response" @exclude-jruby Scenario: New requests result in an error when the cassette file exists Given a file named "error_for_new_requests_when_cassette_exists.rb" with: """ruby require 'setup' VCR.use_cassette('example', :record => :once) do response = Net::HTTP.get_response('localhost', '/', $server.port) puts "Response: #{response.body}" end """ When I run `ruby error_for_new_requests_when_cassette_exists.rb` Then it should fail with "An HTTP request has been made that VCR does not know how to handle" Scenario: New requests get recorded when there is no cassette file Given a file named "record_new_requests.rb" with: """ruby require 'setup' VCR.use_cassette('example', :record => :once) do response = Net::HTTP.get_response('localhost', '/', $server.port) puts "Response: #{response.body}" end """ When I remove the file "cassettes/example.yml" And I run `ruby record_new_requests.rb` Then it should pass with "Response: Hello" And the file "cassettes/example.yml" should contain "Hello" vcr-5.0.0/features/request_matching/000077500000000000000000000000001347305653000174745ustar00rootroot00000000000000vcr-5.0.0/features/request_matching/README.md000066400000000000000000000031341347305653000207540ustar00rootroot00000000000000In order to properly replay previously recorded requests, VCR must match new HTTP requests to a previously recorded one. By default, it matches on HTTP method and URI, since that is usually deterministic and fully identifies the resource and action for typical RESTful APIs. You can customize how VCR matches requests using the `:match_requests_on` cassette option. Specify an array of attributes to match on. Supported attributes are: - `:method` - The HTTP method (i.e. GET, POST, PUT or DELETE) of the request. - `:uri` - The full URI of the request. - `:host` - The host of the URI. You can use this (alone, or in combination with `:path`) as an alternative to `:uri` to cause VCR to match using a regex that matches the host. - `:path` - The path of the URI. You can use this (alone, or in combination with `:host`) as an alternative to `:uri` to cause VCR to match using a regex that matches the path. - `:query` - The query string values of the URI. The query string ordering does not affect matching results (it's order-agnostic). - `:body` - The body of the request. - `:headers` - The request headers. You can also register a custom request matcher. This particularly comes in handy for dealing with APIs that use non-deterministic URIs (i.e. by including a timestamp as a query parameter or whatever). When a cassette contains multiple HTTP interactions that match a request based on the configured `:match_requests_on` setting, the responses are sequenced: the first matching request will get the first response, the second matching request will get the second response, etc. vcr-5.0.0/features/request_matching/body.feature000066400000000000000000000052451347305653000220140ustar00rootroot00000000000000Feature: Matching on Body Use the `:body` request matcher to match requests on the request body. Background: Given a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: post uri: http://example.net/some/long/path body: encoding: UTF-8 string: body1 headers: {} response: status: code: 200 message: OK headers: Content-Length: - "14" body: encoding: UTF-8 string: body1 response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: post uri: http://example.net/some/long/path body: encoding: UTF-8 string: body2 headers: {} response: status: code: 200 message: OK headers: Content-Length: - "14" body: encoding: UTF-8 string: body2 response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Scenario Outline: Replay interaction that matches the body And a file named "body_matching.rb" with: """ruby include_http_adapter_for("") require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' end VCR.use_cassette('example', :match_requests_on => [:body]) do puts "Response for body2: " + response_body_for(:put, "http://example.com/", "body2") end VCR.use_cassette('example', :match_requests_on => [:body]) do puts "Response for body1: " + response_body_for(:put, "http://example.com/", "body1") end """ When I run `ruby body_matching.rb` Then it should pass with: """ Response for body2: body2 response Response for body1: body1 response """ Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :webmock | httpclient | | c.hook_into :webmock | curb | | c.hook_into :webmock | patron | | c.hook_into :webmock | em-http-request | | c.hook_into :webmock | typhoeus | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | | c.hook_into :faraday | faraday (w/ typhoeus) | vcr-5.0.0/features/request_matching/body_as_json.feature000066400000000000000000000054711347305653000235310ustar00rootroot00000000000000Feature: Matching on Body Use the `:body_as_json` request matcher to match requests on the request body where the body is JSON. Background: Given a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: post uri: http://example.net/some/long/path body: encoding: UTF-8 string: '{ "a" : "1" }' headers: {} response: status: code: 200 message: OK headers: Content-Length: - "14" body: encoding: UTF-8 string: body1 response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: post uri: http://example.net/some/long/path body: encoding: UTF-8 string: '{ "a" : "1", "b" : "2" }' headers: {} response: status: code: 200 message: OK headers: Content-Length: - "14" body: encoding: UTF-8 string: body2 response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Scenario Outline: Replay interaction that matches the body as JSON And a file named "body_as_json_matching.rb" with: """ruby include_http_adapter_for("") require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' end VCR.use_cassette('example', :match_requests_on => [:body_as_json]) do puts "Response for body as json 2: " + response_body_for(:put, "http://example.com/", '{ "a" : "1", "b" : "2" }') end VCR.use_cassette('example', :match_requests_on => [:body_as_json]) do puts "Response for body as json 1: " + response_body_for(:put, "http://example.com/", '{ "a" : "1" }') end """ When I run `ruby body_as_json_matching.rb` Then it should pass with: """ Response for body as json 2: body2 response Response for body as json 1: body1 response """ Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :webmock | httpclient | | c.hook_into :webmock | curb | | c.hook_into :webmock | patron | | c.hook_into :webmock | em-http-request | | c.hook_into :webmock | typhoeus | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | | c.hook_into :faraday | faraday (w/ typhoeus) | vcr-5.0.0/features/request_matching/custom_matcher.feature000066400000000000000000000103541347305653000240710ustar00rootroot00000000000000Feature: Register and use a custom matcher If the built-in matchers do not meet your needs, you can use a custom matcher. Any 2-argument callable (that is, an object that responds to #call and accepts 2 arguments) can be a matcher. Simply put the callable in your `:match_requests_on` array. In addition, you can register a named custom matcher with VCR, and use the name in your `:match_requests_on` array. Either way, your custom matcher should return a truthy value if the given requests should be considered equivalent. Background: Given a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: get uri: http://foo.com:9000/foo body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "18" body: encoding: UTF-8 string: port 9000 response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: get uri: http://foo.com:8000/foo body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "18" body: encoding: UTF-8 string: port 8000 response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Scenario Outline: Use a callable as a custom request matcher And a file named "callable_matcher.rb" with: """ruby include_http_adapter_for("") require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' end port_matcher = lambda do |request_1, request_2| URI(request_1.uri).port == URI(request_2.uri).port end VCR.use_cassette('example', :match_requests_on => [:method, port_matcher]) do puts "Response for port 8000: " + response_body_for(:get, "http://example.com:8000/") end VCR.use_cassette('example', :match_requests_on => [:method, port_matcher]) do puts "Response for port 9000: " + response_body_for(:get, "http://example.com:9000/") end """ When I run `ruby callable_matcher.rb` Then it should pass with: """ Response for port 8000: port 8000 response Response for port 9000: port 9000 response """ Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :webmock | httpclient | | c.hook_into :webmock | curb | | c.hook_into :webmock | patron | | c.hook_into :webmock | em-http-request | | c.hook_into :webmock | typhoeus | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | | c.hook_into :faraday | faraday (w/ typhoeus) | Scenario: Register a named custom matcher And a file named "register_custom_matcher.rb" with: """ruby include_http_adapter_for("net/http") require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' 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('example', :match_requests_on => [:method, :port]) do puts "Response for port 8000: " + response_body_for(:get, "http://example.com:8000/") end VCR.use_cassette('example', :match_requests_on => [:method, :port]) do puts "Response for port 9000: " + response_body_for(:get, "http://example.com:9000/") end """ When I run `ruby register_custom_matcher.rb` Then it should pass with: """ Response for port 8000: port 8000 response Response for port 9000: port 9000 response """ vcr-5.0.0/features/request_matching/headers.feature000066400000000000000000000046261347305653000224740ustar00rootroot00000000000000Feature: Matching on Headers Use the `:headers` request matcher to match requests on the request headers. Scenario Outline: Replay interaction that matches the headers Given a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: post uri: http://example.net/some/long/path body: encoding: UTF-8 string: "" headers: X-User-Id: - "1" response: status: code: 200 message: OK headers: Content-Length: - "15" body: encoding: UTF-8 string: user 1 response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: post uri: http://example.net/some/long/path body: encoding: UTF-8 string: "" headers: X-User-Id: - "2" response: status: code: 200 message: OK headers: Content-Length: - "15" body: encoding: UTF-8 string: user 2 response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ And a file named "header_matching.rb" with: """ruby include_http_adapter_for("") require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' end VCR.use_cassette('example', :match_requests_on => [:headers]) do puts "Response for user 2: " + response_body_for(:get, "http://example.com/", nil, 'X-User-Id' => '2') end VCR.use_cassette('example', :match_requests_on => [:headers]) do puts "Response for user 1: " + response_body_for(:get, "http://example.com/", nil, 'X-User-Id' => '1') end """ When I run `ruby header_matching.rb` Then it should pass with: """ Response for user 2: user 2 response Response for user 1: user 1 response """ Examples: | configuration | http_lib | | c.hook_into :webmock | curb | | c.hook_into :webmock | patron | | c.hook_into :webmock | em-http-request | vcr-5.0.0/features/request_matching/host.feature000066400000000000000000000055231347305653000220330ustar00rootroot00000000000000Feature: Matching on Host Use the `:host` request matcher to match requests on the request host. You can use this (alone, or in combination with `:path`) as an alternative to `:uri` so that non-deterministic portions of the URI are not considered as part of the request matching. Background: Given a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: post uri: http://host1.com/some/long/path body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "14" body: encoding: UTF-8 string: host1 response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: post uri: http://host2.com/some/other/long/path body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "16" body: encoding: UTF-8 string: host2 response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Scenario Outline: Replay interaction that matches the host And a file named "host_matching.rb" with: """ruby include_http_adapter_for("") require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' end VCR.use_cassette('example', :match_requests_on => [:host]) do puts "Response for host2: " + response_body_for(:get, "http://host2.com/home") end VCR.use_cassette('example', :match_requests_on => [:host]) do puts "Response for host1: " + response_body_for(:get, "http://host1.com/about") end """ When I run `ruby host_matching.rb` Then it should pass with: """ Response for host2: host2 response Response for host1: host1 response """ Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :webmock | httpclient | | c.hook_into :webmock | curb | | c.hook_into :webmock | patron | | c.hook_into :webmock | em-http-request | | c.hook_into :webmock | typhoeus | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | | c.hook_into :faraday | faraday (w/ typhoeus) | vcr-5.0.0/features/request_matching/identical_request_sequence.feature000066400000000000000000000052121347305653000264450ustar00rootroot00000000000000Feature: Identical requests are replayed in sequence When a cassette contains multiple HTTP interactions that match a request based on the configured `:match_requests_on` setting, the responses are sequenced: the first matching request will get the first response, the second matching request will get the second response, etc. Scenario Outline: identical requests are replayed in sequence Given a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: get uri: http://example.com/foo body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "10" body: encoding: UTF-8 string: Response 1 http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: get uri: http://example.com/foo body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "10" body: encoding: UTF-8 string: Response 2 http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ And a file named "rotate_responses.rb" with: """ruby include_http_adapter_for("") require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' end VCR.use_cassette('example') do puts response_body_for(:get, 'http://example.com/foo') puts response_body_for(:get, 'http://example.com/foo') end """ When I run `ruby rotate_responses.rb` Then it should pass with: """ Response 1 Response 2 """ Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :webmock | httpclient | | c.hook_into :webmock | curb | | c.hook_into :webmock | patron | | c.hook_into :webmock | em-http-request | | c.hook_into :webmock | typhoeus | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | | c.hook_into :faraday | faraday (w/ typhoeus) | vcr-5.0.0/features/request_matching/method.feature000066400000000000000000000055251347305653000223400ustar00rootroot00000000000000Feature: Matching on Method Use the `:method` request matcher to match requests on the HTTP method (i.e. GET, POST, PUT, DELETE, etc). You will generally want to use this matcher. The `:method` matcher is used (along with the `:uri` matcher) by default if you do not specify how requests should match. Background: Given a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: post uri: http://post-request.com/ body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "13" body: encoding: UTF-8 string: post response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: get uri: http://get-request.com/ body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "12" body: encoding: UTF-8 string: get response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Scenario Outline: Replay interaction that matches the HTTP method And a file named "method_matching.rb" with: """ruby include_http_adapter_for("") require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' end VCR.use_cassette('example', :match_requests_on => [:method]) do puts "Response for GET: " + response_body_for(:get, "http://example.com/") end VCR.use_cassette('example', :match_requests_on => [:method]) do puts "Response for POST: " + response_body_for(:post, "http://example.com/") end """ When I run `ruby method_matching.rb` Then it should pass with: """ Response for GET: get response Response for POST: post response """ Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :webmock | httpclient | | c.hook_into :webmock | curb | | c.hook_into :webmock | patron | | c.hook_into :webmock | em-http-request | | c.hook_into :webmock | typhoeus | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | | c.hook_into :faraday | faraday (w/ typhoeus) | vcr-5.0.0/features/request_matching/path.feature000066400000000000000000000055631347305653000220160ustar00rootroot00000000000000Feature: Matching on Path Use the `:path` request matcher to match requests on the path portion of the request URI. You can use this (alone, or in combination with `:host`) as an alternative to `:uri` so that non-deterministic portions of the URI are not considered as part of the request matching. Background: Given a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: post uri: http://host1.com/about?date=2011-09-01 body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "14" body: encoding: UTF-8 string: about response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: post uri: http://host2.com/home?date=2011-09-01 body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "15" body: encoding: UTF-8 string: home response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Scenario Outline: Replay interaction that matches the path And a file named "path_matching.rb" with: """ruby include_http_adapter_for("") require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' end VCR.use_cassette('example', :match_requests_on => [:path]) do puts "Response for /home: " + response_body_for(:get, "http://example.com/home") end VCR.use_cassette('example', :match_requests_on => [:path]) do puts "Response for /about: " + response_body_for(:get, "http://example.com/about") end """ When I run `ruby path_matching.rb` Then it should pass with: """ Response for /home: home response Response for /about: about response """ Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :webmock | httpclient | | c.hook_into :webmock | curb | | c.hook_into :webmock | patron | | c.hook_into :webmock | em-http-request | | c.hook_into :webmock | typhoeus | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | | c.hook_into :faraday | faraday (w/ typhoeus) | vcr-5.0.0/features/request_matching/playback_repeats.feature000066400000000000000000000060031347305653000243610ustar00rootroot00000000000000Feature: Playback repeats By default, each response in a cassette can only be matched and played back once while the cassette is in use (it can, of course, be re-used in multiple tests, each of which should use the cassette separately). Note that this is a change from the behavior in VCR 1.x. The old behavior occurred because of how WebMock behave internally and was not intended. Repeats create less accurate tests since the real HTTP server may not necessarily return the same response when identical requests are made in sequence. If you want to allow playback repeats, VCR has a cassette option for this: :allow_playback_repeats => true @exclude-jruby Scenario: Responses do not repeat by default Given a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: get uri: http://example.com/foo body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "10" body: encoding: UTF-8 string: Response 1 http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: get uri: http://example.com/foo body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "10" body: encoding: UTF-8 string: Response 2 http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ And a file named "playback_repeats.rb" with: """ruby include_http_adapter_for("net/http") require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' end puts "== With :allow_playback_repeats ==" VCR.use_cassette('example', :allow_playback_repeats => true) do puts response_body_for(:get, 'http://example.com/foo') puts response_body_for(:get, 'http://example.com/foo') puts response_body_for(:get, 'http://example.com/foo') end puts "\n== Without :allow_playback_repeats ==" VCR.use_cassette('example') do puts response_body_for(:get, 'http://example.com/foo') puts response_body_for(:get, 'http://example.com/foo') puts response_body_for(:get, 'http://example.com/foo') end """ When I run `ruby playback_repeats.rb` Then it should fail with "An HTTP request has been made that VCR does not know how to handle" And the output should contain: """ == With :allow_playback_repeats == Response 1 Response 2 Response 2 == Without :allow_playback_repeats == Response 1 Response 2 """ vcr-5.0.0/features/request_matching/query.feature000066400000000000000000000060551347305653000222240ustar00rootroot00000000000000Feature: Matching on Query string Use the `:query` request matcher to match requests on the query string portion of the request URI. You can use this (alone, or in combination with others) as an alternative to `:uri` so that non-deterministic portions of the URI are not considered as part of the request matching. Background: Given a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: post uri: http://host1.com/query?date=2011-09-01 body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "19" body: encoding: UTF-8 string: 2011-09-01 response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: post uri: http://host1.com/query?date=2011-09-02 body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "19" body: encoding: UTF-8 string: 2011-09-02 response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Scenario Outline: Replay interaction that matches the query string And a file named "query_matching.rb" with: """ruby include_http_adapter_for("") require 'vcr' VCR.configure do |c| c.default_cassette_options = { :match_requests_on => [:query] } c.cassette_library_dir = 'cassettes' end VCR.use_cassette('example', :match_requests_on => [:query]) do puts "Response for 2011-09-01 /query: " + response_body_for(:get, "http://example.com/query?date=2011-09-01") end VCR.use_cassette('example', :match_requests_on => [:query]) do puts "Response for 2011-09-02 /query: " + response_body_for(:get, "http://example.com/query?date=2011-09-02") end """ When I run `ruby query_matching.rb` Then it should pass with: """ Response for 2011-09-01 /query: 2011-09-01 response Response for 2011-09-02 /query: 2011-09-02 response """ Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :webmock | httpclient | | c.hook_into :webmock | curb | | c.hook_into :webmock | patron | | c.hook_into :webmock | em-http-request | | c.hook_into :webmock | typhoeus | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | | c.hook_into :faraday | faraday (w/ typhoeus) | vcr-5.0.0/features/request_matching/uri.feature000066400000000000000000000053611347305653000216550ustar00rootroot00000000000000Feature: Matching on URI Use the `:uri` request matcher to match requests on the request URI. The `:uri` matcher is used (along with the `:method` matcher) by default if you do not specify how requests should match. Background: Given a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: post uri: http://example.com/foo body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "12" body: encoding: UTF-8 string: foo response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: post uri: http://example.com/bar body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "12" body: encoding: UTF-8 string: bar response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Scenario Outline: Replay interaction that matches the request URI And a file named "uri_matching.rb" with: """ruby include_http_adapter_for("") require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'cassettes' end VCR.use_cassette('example', :match_requests_on => [:uri]) do puts "Response for /bar: " + response_body_for(:get, "http://example.com/bar") end VCR.use_cassette('example', :match_requests_on => [:uri]) do puts "Response for /foo: " + response_body_for(:get, "http://example.com/foo") end """ When I run `ruby uri_matching.rb` Then it should pass with: """ Response for /bar: bar response Response for /foo: foo response """ Examples: | configuration | http_lib | | c.hook_into :webmock | net/http | | c.hook_into :webmock | httpclient | | c.hook_into :webmock | curb | | c.hook_into :webmock | patron | | c.hook_into :webmock | em-http-request | | c.hook_into :webmock | typhoeus | | c.hook_into :typhoeus | typhoeus | | c.hook_into :excon | excon | | c.hook_into :faraday | faraday (w/ net_http) | | c.hook_into :faraday | faraday (w/ typhoeus) | vcr-5.0.0/features/request_matching/uri_without_param.feature000066400000000000000000000055441347305653000246230ustar00rootroot00000000000000Feature: URI without param(s) A common source of difficulty when using VCR with the default matchers are non-deterministic URIs. If the URI changes on every test run (because it includes a timestamp parameter, or whatever), the default URI matcher will not work well for you. You can write a custom matcher to match URIs however you want, but for the common need to match on a URI and ignore particular query parameters, VCR provides an easier way: :match_requests_on => [ :method, VCR.request_matchers.uri_without_param(:timestamp) ] `uri_without_param` also has a plural alias (i.e. `uri_without_params(:timestamp, :session)`) Background: Given a previously recorded cassette file "cassettes/example.yml" with: """ --- http_interactions: - request: method: get uri: http://example.com/search?q=foo×tamp=1316920490 body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "12" body: encoding: UTF-8 string: foo response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT - request: method: get uri: http://example.com/search?q=bar×tamp=1296723437 body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "12" body: encoding: UTF-8 string: bar response http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Scenario: Match the URI on all but the timestamp query parameter And a file named "uri_without_param_matcher.rb" with: """ruby include_http_adapter_for("net/http") require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'cassettes' c.default_cassette_options = { :match_requests_on => [:method, VCR.request_matchers.uri_without_param(:timestamp)] } end def search_uri(q) "http://example.com/search?q=#{q}×tamp=#{Time.now.to_i}" end VCR.use_cassette('example') do puts "Response for bar: " + response_body_for(:get, search_uri("bar")) end VCR.use_cassette('example') do puts "Response for foo: " + response_body_for(:get, search_uri("foo")) end """ When I run `ruby uri_without_param_matcher.rb` Then it should pass with: """ Response for bar: bar response Response for foo: foo response """ vcr-5.0.0/features/step_definitions/000077500000000000000000000000001347305653000175005ustar00rootroot00000000000000vcr-5.0.0/features/step_definitions/cli_steps.rb000066400000000000000000000156411347305653000220210ustar00rootroot00000000000000require 'vcr' require 'multi_json' module VCRHelpers def normalize_cassette_hash(cassette_hash) cassette_hash['recorded_with'] = "VCR #{VCR.version}" cassette_hash['http_interactions'].map! { |h| normalize_http_interaction(h) } cassette_hash end def normalize_headers(object) object.headers = {} and return if object.headers.nil? object.headers = {}.tap do |hash| object.headers.each do |key, value| hash[key.downcase] = value end end end def static_timestamp @static_timestamp ||= Time.now end def normalize_http_interaction(hash) VCR::HTTPInteraction.from_hash(hash).tap do |i| normalize_headers(i.request) normalize_headers(i.response) i.recorded_at &&= static_timestamp i.request.body ||= '' i.response.body ||= '' i.response.status.message ||= '' i.response.adapter_metadata.clear # Remove non-deterministic headers and headers # that get added by a particular HTTP library (but not by others) i.response.headers.reject! { |k, v| %w[ server date connection ].include?(k) } i.request.headers.reject! { |k, v| %w[ accept user-agent connection expect date ].include?(k) } # Some HTTP libraries include an extra space ("OK " instead of "OK") i.response.status.message = i.response.status.message.strip if @scenario_parameters.to_s =~ /excon|faraday/ # Excon/Faraday do not expose the status message or http version, # so we have no way to record these attributes. i.response.status.message = nil i.response.http_version = nil elsif @scenario_parameters.to_s.include?('webmock') # WebMock does not expose the HTTP version so we have no way to record it i.response.http_version = nil end end end def normalize_cassette_content(content) return content unless @scenario_parameters.to_s.include?('patron') cassette_hash = YAML.load(content) cassette_hash['http_interactions'].map! do |hash| VCR::HTTPInteraction.from_hash(hash).tap do |i| i.request.headers = (i.request.headers || {}).merge!('Expect' => ['']) end.to_hash end YAML.dump(cassette_hash) end def modify_file(file_name, orig_text, new_text) in_current_dir do file = File.read(file_name) regex = /#{Regexp.escape(orig_text)}/ expect(file).to match(regex) file = file.gsub(regex, new_text) File.open(file_name, 'w') { |f| f.write(file) } end end end World(VCRHelpers) Given(/the following files do not exist:/) do |files| check_file_presence(files.raw.map{|file_row| file_row[0]}, false) end Given(/^the directory "([^"]*)" does not exist$/) do |dir| check_directory_presence([dir], false) end Given(/^a previously recorded cassette file "([^"]*)" with:$/) do |file_name, content| write_file(file_name, normalize_cassette_content(content)) end Given(/^it is (.*)$/) do |date_string| set_env('DATE_STRING', date_string) end Given(/^that port numbers in "([^"]*)" are normalized to "([^"]*)"$/) do |file_name, port| in_current_dir do contents = File.read(file_name) contents = contents.gsub(/:\d{2,}\//, ":#{port}/") File.open(file_name, 'w') { |f| f.write(contents) } end end When(/^I modify the file "([^"]*)" to replace "([^"]*)" with "([^"]*)"$/) do |file_name, orig_text, new_text| modify_file(file_name, orig_text, new_text) end When(/^I append to file "([^"]*)":$/) do |file_name, content| append_to_file(file_name, "\n" + content) end When(/^I set the "([^"]*)" environment variable to "([^"]*)"$/) do |var, value| set_env(var, value) end Then(/^the file "([^"]*)" should exist$/) do |file_name| check_file_presence([file_name], true) end Then(/^it should (pass|fail) with "([^"]*)"$/) do |pass_fail, partial_output| assert_exit_status_and_partial_output(pass_fail == 'pass', partial_output) end Then(/^it should (pass|fail) with an error like:$/) do |pass_fail, partial_output| assert_success(pass_fail == 'pass') # different implementations place the exception class at different # places relative to the message (i.e. with a multiline error message) process_output = all_output.gsub(/\s*\(VCR::Errors::\w+\)/, '') # Some implementations include extra leading spaces, for some reason... process_output.gsub!(/^\s*/, '') partial_output.gsub!(/^\s*/, '') assert_partial_output(partial_output, process_output) end Then(/^the output should contain each of the following:$/) do |table| table.raw.flatten.each do |string| assert_partial_output(string, all_output) end end Then(/^the file "([^"]*)" should contain YAML like:$/) do |file_name, expected_content| actual_content = in_current_dir { File.read(file_name) } expect(normalize_cassette_hash(YAML.load(actual_content))).to eq(normalize_cassette_hash(YAML.load(expected_content.to_s))) end Then(/^the file "([^"]*)" should contain JSON like:$/) do |file_name, expected_content| actual_content = in_current_dir { File.read(file_name) } actual = MultiJson.decode(actual_content) expected = MultiJson.decode(expected_content.to_s) expect(normalize_cassette_hash(actual)).to eq(normalize_cassette_hash(expected)) end Then(/^the file "([^"]*)" should contain compressed YAML like:$/) do |file_name, expected_content| actual_content = in_current_dir { File.read(file_name) } unzipped_content = Zlib::Inflate.inflate(actual_content) expect(normalize_cassette_hash(YAML.load(unzipped_content))).to eq(normalize_cassette_hash(YAML.load(expected_content.to_s))) end Then(/^the file "([^"]*)" should contain ruby like:$/) do |file_name, expected_content| actual_content = in_current_dir { File.read(file_name) } actual = eval(actual_content) expected = eval(expected_content) expect(normalize_cassette_hash(actual)).to eq(normalize_cassette_hash(expected)) end Then(/^the file "([^"]*)" should contain each of these:$/) do |file_name, table| table.raw.flatten.each do |string| check_file_content(file_name, string, true) end end Then(/^the file "([^"]*)" should contain a YAML fragment like:$/) do |file_name, fragment| in_current_dir do file_content = File.read(file_name) # Normalize by removing leading and trailing whitespace... file_content = file_content.split("\n").map do |line| # Different versions of psych use single vs. double quotes # And then 2.1 sometimes adds quotes... line.strip.gsub('"', "'").gsub("'", '') end.join("\n") expect(file_content).to include(fragment.gsub("'", '')) end end Then(/^the cassette "([^"]*)" should have the following response bodies:$/) do |file, table| interactions = in_current_dir { YAML.load_file(file) }['http_interactions'].map { |h| VCR::HTTPInteraction.from_hash(h) } actual_response_bodies = interactions.map { |i| i.response.body } expected_response_bodies = table.raw.flatten expect(actual_response_bodies).to match(expected_response_bodies) end Then(/^it should (pass|fail)$/) do |pass_fail| assert_success(pass_fail == 'pass') end vcr-5.0.0/features/support/000077500000000000000000000000001347305653000156465ustar00rootroot00000000000000vcr-5.0.0/features/support/env.rb000066400000000000000000000022211347305653000167600ustar00rootroot00000000000000require 'bundler' Bundler.setup ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby" require 'aruba/cucumber' require 'aruba/jruby' if RUBY_PLATFORM == 'java' gem_specs = Bundler.load.specs load_paths = Dir.glob(gem_specs.map { |spec| if spec.respond_to?(:lib_dirs_glob) spec.lib_dirs_glob else spec.load_paths end }.flatten) load_paths << File.expand_path("../../../spec", __FILE__) rubyopt = "-rsupport/cucumber_helpers" if RUBY_VERSION > '1.9' load_paths.unshift(".") rubyopt = "--disable-gems #{rubyopt}" unless "rbx" == ruby_engine end Before do @aruba_timeout_seconds = 30 if "jruby" == ruby_engine @aruba_io_wait_seconds = 0.1 else @aruba_io_wait_seconds = 0.02 end end Before("~@with-bundler") do set_env("RUBYLIB", load_paths.join(":")) set_env("RUBYOPT", rubyopt) set_env("RBXOPT", "--disable-gems #{ENV["RBXOPT"]}") if "rbx" == ruby_engine set_env("GEM_HOME", nil) end Before("@with-bundler") do set_env("RUBYLIB", ".:#{ENV["RUBYLIB"]}:#{load_paths.last}") set_env("RUBYOPT", "#{ENV["RUBYOPT"]} -rsupport/cucumber_helpers") set_env("BUNDLE_GEMFILE", Bundler.default_gemfile.expand_path.to_s) end vcr-5.0.0/features/support/http_lib_filters.rb000066400000000000000000000033201347305653000215260ustar00rootroot00000000000000if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx' # Patron is freezing up the cukes (as it does on 1.9.2) # I'm not sure why em-http-request isn't working on rbx, # but considering the fact that VCR works with all the other # libs just fine and doesn't do anything for em-http-request, # it's probably a bug in it or rbx...so ignore it, for now. # I'm getting errors in the curb C extension in rbx. # Typhoeus should be buildable on rbx, but the travis build times out, # so we skip them to speed up the build on travis. UNSUPPORTED_HTTP_LIBS = %w[ patron em-http-request curb typhoeus ] elsif RUBY_PLATFORM == 'java' # These gems have C extensions and can't install on JRuby. c_dependent_libs = %w[ typhoeus patron curb em-http-request ] # The latest version of httpclient seems to freeze up the cukes # on JRuby. I'm not sure why, and there's little benefit to running # them on JRuby...so we just skip them. Excon seems to have the same issue :(. UNSUPPORTED_HTTP_LIBS = c_dependent_libs + %w[ httpclient excon ] end if defined?(UNSUPPORTED_HTTP_LIBS) UNSUPPORTED_HTTP_LIB_REGEX = Regexp.union(*UNSUPPORTED_HTTP_LIBS) # Filter out example rows that use libraries that are not supported on the current ruby interpreter Before do |scenario| if scenario.respond_to?(:cell_values) && scenario.cell_values.any? { |v| v =~ UNSUPPORTED_HTTP_LIB_REGEX } scenario.skip_invoke! end end end # Set a global based on the current stubbing lib so we can put special-case # logic in our step definitions based on the http stubbing library. Before do |scenario| if scenario.respond_to?(:cell_values) @scenario_parameters = scenario.cell_values else @scenario_parameters = nil end end vcr-5.0.0/features/test_frameworks/000077500000000000000000000000001347305653000173515ustar00rootroot00000000000000vcr-5.0.0/features/test_frameworks/cucumber.feature000066400000000000000000000206521347305653000225400ustar00rootroot00000000000000@with-bundler Feature: Usage with Cucumber VCR can be used with cucumber in two basic ways: - Use `VCR.use_cassette` in a step definition. - Use a `VCR.cucumber_tags` block to tell VCR to use a cassette for a tagged scenario. In a cucumber support file (e.g. features/support/vcr.rb), put code like this: ``` ruby VCR.cucumber_tags do |t| t.tag '@tag1' t.tags '@tag2', '@tag3' t.tag '@tag3', :cassette => :options t.tags '@tag4', '@tag5', :cassette => :options t.tag '@vcr', :use_scenario_name => true end ``` VCR will use a cassette named `cucumber_tags/` for scenarios with each of these tags (Unless the `:use_scenario_name` option is provided. See below). The configured `default_cassette_options` will be used, or you can override specific options by passing a hash as the last argument to `#tag` or `#tags`. You can also have VCR name your cassettes automatically according to the feature and scenario name by providing `:use_scenario_name => true` to `#tag` or `#tags`. In this case, the cassette will be named `/`. For scenario outlines, VCR will record one cassette per row, and the cassettes will be named `//`. @exclude-jruby Scenario: Record HTTP interactions in a scenario by tagging it Given a file named "lib/server.rb" with: """ruby if ENV['WITH_SERVER'] == 'true' $server = start_sinatra_app do get('/:path') { "Hello #{params[:path]}" } end end """ Given a file named "features/support/vcr.rb" with: """ruby require "lib/server" require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'features/cassettes' c.default_cassette_options = { :match_requests_on => [:method, :host, :path] } end VCR.cucumber_tags do |t| t.tag '@localhost_request' # uses default record mode since no options are given t.tags '@disallowed_1', '@disallowed_2', :record => :none t.tag '@vcr', :use_scenario_name => true end """ And a file named "features/step_definitions/steps.rb" with: """ruby require 'net/http' When /^a request is made to "([^"]*)"$/ do |url| uri = URI.parse(url) uri.port = $server.port if $server @response = Net::HTTP.get_response(uri) end When /^(.*) within a cassette named "([^"]*)"$/ do |step_name, cassette_name| VCR.use_cassette(cassette_name) { step(step_name) } end Then /^the response should be "([^"]*)"$/ do |expected_response| expect(@response.body).to eq(expected_response) end """ And a file named "features/vcr_example.feature" with: """ Feature: VCR example Note: Cucumber treats the pre-amble as part of the feature name. When using the :use_scenario_name option, VCR will only use the first line of the feature name as the directory for the cassette. @localhost_request Scenario: tagged scenario When a request is made to "http://localhost:7777/localhost_request_1" Then the response should be "Hello localhost_request_1" When a request is made to "http://localhost:7777/nested_cassette" within a cassette named "nested_cassette" Then the response should be "Hello nested_cassette" When a request is made to "http://localhost:7777/localhost_request_2" Then the response should be "Hello localhost_request_2" @vcr Scenario: tagged scenario Note: Like the feature pre-amble, Cucumber treats the scenario pre-amble as part of the scenario name. When using the :use_scenario_name option, VCR will only use the first line of the feature name as the directory for the cassette. When a request is made to "http://localhost:7777/localhost_request_1" Then the response should be "Hello localhost_request_1" @vcr Scenario Outline: tagged scenario outline When a request is made to "http://localhost:7777/localhost_request_1" Then the response should be "Hello localhost_request_1" Examples: | key | value | | foo | bar | @disallowed_1 Scenario: tagged scenario When a request is made to "http://localhost:7777/allowed" within a cassette named "allowed" Then the response should be "Hello allowed" When a request is made to "http://localhost:7777/disallowed_1" @disallowed_2 Scenario: tagged scenario When a request is made to "http://localhost:7777/disallowed_2" """ And the directory "features/cassettes" does not exist When I run `cucumber WITH_SERVER=true features/vcr_example.feature` Then it should fail with "5 scenarios (2 failed, 3 passed)" And the file "features/cassettes/cucumber_tags/localhost_request.yml" should contain "Hello localhost_request_1" And the file "features/cassettes/cucumber_tags/localhost_request.yml" should contain "Hello localhost_request_2" And the file "features/cassettes/nested_cassette.yml" should contain "Hello nested_cassette" And the file "features/cassettes/allowed.yml" should contain "Hello allowed" And the file "features/cassettes/VCR_example/tagged_scenario.yml" should contain "Hello localhost_request_1" And the file "features/cassettes/VCR_example/tagged_scenario_outline/_foo_bar_.yml" should contain "Hello localhost_request_1" # Run again without the server; we'll get the same responses because VCR # will replay the recorded responses. When I run `cucumber features/vcr_example.feature` Then it should fail with "5 scenarios (2 failed, 3 passed)" And the output should contain each of the following: | An HTTP request has been made that VCR does not know how to handle: | | GET http://localhost:7777/disallowed_1 | | An HTTP request has been made that VCR does not know how to handle: | | GET http://localhost:7777/disallowed_2 | And the file "features/cassettes/cucumber_tags/localhost_request.yml" should contain "Hello localhost_request_1" And the file "features/cassettes/cucumber_tags/localhost_request.yml" should contain "Hello localhost_request_2" And the file "features/cassettes/nested_cassette.yml" should contain "Hello nested_cassette" And the file "features/cassettes/allowed.yml" should contain "Hello allowed" And the file "features/cassettes/VCR_example/tagged_scenario.yml" should contain "Hello localhost_request_1" And the file "features/cassettes/VCR_example/tagged_scenario_outline/_foo_bar_.yml" should contain "Hello localhost_request_1" Scenario: `:allow_unused_http_interactions => false` does not raise if the scenario already failed Given a previously recorded cassette file "features/cassettes/cucumber_tags/example.yml" with: """ --- http_interactions: - request: method: get uri: http://example.com/foo body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "5" body: encoding: UTF-8 string: Hello http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ And a file named "features/support/vcr.rb" with: """ruby require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'features/cassettes' end VCR.cucumber_tags do |t| t.tag '@example', :allow_unused_http_interactions => false end """ And a file named "features/step_definitions/steps.rb" with: """ruby When /^the scenario fails$/ do raise "boom" end """ And a file named "features/vcr_example.feature" with: """ Feature: @example Scenario: tagged scenario When the scenario fails """ When I run `cucumber features/vcr_example.feature` Then it should fail with "1 scenario (1 failed)" And the output should contain "boom" And the output should not contain "There are unused HTTP interactions" vcr-5.0.0/features/test_frameworks/rspec_metadata.feature000066400000000000000000000115541347305653000237100ustar00rootroot00000000000000@with-bundler Feature: Usage with RSpec metadata VCR provides easy integration with RSpec using metadata. To set this up, call `configure_rspec_metadata!` in your `VCR.configure` block. Once you've done that, you can have an example group or example use VCR by passing `:vcr` as an additional argument after the description string. It will set the cassette name based on the example's full description. If you need to override the cassette name or options, you can pass a hash (`:vcr => { ... }`). Background: Given a file named "spec/spec_helper.rb" with: """ruby require 'vcr' VCR.configure do |c| c.cassette_library_dir = 'spec/cassettes' c.hook_into :webmock c.configure_rspec_metadata! end RSpec.configure do |c| # so we can use `:vcr` rather than `:vcr => true`; # in RSpec 3 this will no longer be necessary. c.treat_symbols_as_metadata_keys_with_true_values = true end """ And a previously recorded cassette file "spec/cassettes/Group/optionally_raises_an_error.yml" with: """ --- http_interactions: - request: method: get uri: http://example.com/foo body: encoding: UTF-8 string: "" headers: {} response: status: code: 200 message: OK headers: Content-Length: - "5" body: encoding: UTF-8 string: Hello http_version: "1.1" recorded_at: Tue, 01 Nov 2011 04:58:44 GMT recorded_with: VCR 2.0.0 """ Scenario: Use `:vcr` metadata Given a file named "spec/vcr_example_spec.rb" with: """ruby $server = start_sinatra_app do get('/') { "Hello" } end def make_http_request Net::HTTP.get_response('localhost', '/', $server.port).body end require 'spec_helper' describe "VCR example group metadata", :vcr do it 'records an http request' do expect(make_http_request).to eq('Hello') end it 'records another http request' do expect(make_http_request).to eq('Hello') end context 'in a nested example group' do it 'records another one' do expect(make_http_request).to eq('Hello') end end end describe "VCR example metadata" do it 'records an http request', :vcr do expect(make_http_request).to eq('Hello') end end """ When I run `rspec spec/vcr_example_spec.rb` Then it should pass with "4 examples, 0 failures" And the file "spec/cassettes/VCR_example_group_metadata/records_an_http_request.yml" should contain "Hello" And the file "spec/cassettes/VCR_example_group_metadata/records_another_http_request.yml" should contain "Hello" And the file "spec/cassettes/VCR_example_group_metadata/in_a_nested_example_group/records_another_one.yml" should contain "Hello" And the file "spec/cassettes/VCR_example_metadata/records_an_http_request.yml" should contain "Hello" Scenario: `:allow_unused_http_interactions => false` causes a failure if there are unused interactions And a file named "spec/vcr_example_spec.rb" with: """ruby require 'spec_helper' describe "Group", :vcr => { :allow_unused_http_interactions => false } do it 'optionally raises an error' do # don't fail end end """ When I run `rspec spec/vcr_example_spec.rb` Then it should fail with an error like: """ There are unused HTTP interactions left in the cassette: - [get http://example.com/foo] => [200 "Hello"] """ Scenario: `:allow_unused_http_interactions => false` does not raise if the example already failed And a file named "spec/vcr_example_spec.rb" with: """ruby require 'spec_helper' describe "Group", :vcr => { :allow_unused_http_interactions => false } do it 'optionally raises an error' do raise "boom" end end """ When I run `rspec spec/vcr_example_spec.rb` Then it should fail with "boom" And the output should not contain "There are unused HTTP interactions" Scenario: Pass a hash to set the cassette options Given a file named "spec/vcr_example_spec.rb" with: """ruby require 'spec_helper' vcr_options = { :cassette_name => "example", :record => :new_episodes } describe "Using an options hash", :vcr => vcr_options do it 'uses the provided cassette name' do expect(VCR.current_cassette.name).to eq("example") end it 'sets the given options' do expect(VCR.current_cassette.record_mode).to eq(:new_episodes) end end """ When I run `rspec spec/vcr_example_spec.rb` Then it should pass with "2 examples, 0 failures" vcr-5.0.0/features/test_frameworks/test_unit.feature000066400000000000000000000033441347305653000227500ustar00rootroot00000000000000Feature: Usage with Test::Unit To use VCR with Test::Unit, wrap the body of any test method in `VCR.use_cassette`. Scenario: Use `VCR.use_cassette` in a test Given a file named "test/test_server.rb" with: """ruby $server = start_sinatra_app do get('/') { "Hello" } end """ Given a file named "test/test_helper.rb" with: """ruby require 'test/test_server' if ENV['SERVER'] == 'true' require 'test/unit' require 'vcr' VCR.configure do |c| c.hook_into :webmock c.cassette_library_dir = 'test/fixtures/vcr_cassettes' c.default_cassette_options = { :match_requests_on => [:method, :host, :path] } end """ And a file named "test/vcr_example_test.rb" with: """ruby require 'test_helper' class VCRExampleTest < Test::Unit::TestCase def test_use_vcr VCR.use_cassette('test_unit_example') do response = Net::HTTP.get_response('localhost', '/', $server ? $server.port : 0) assert_equal "Hello", response.body end end end """ And the directory "test/fixtures/vcr_cassettes" does not exist When I set the "SERVER" environment variable to "true" And I run `ruby -Itest test/vcr_example_test.rb` Then it should pass with "1 tests, 1 assertions, 0 failures, 0 errors" And the file "test/fixtures/vcr_cassettes/test_unit_example.yml" should contain "Hello" # Run again without starting the sinatra server so the response will be replayed When I set the "SERVER" environment variable to "false" And I run `ruby -Itest test/vcr_example_test.rb` Then it should pass with "1 tests, 1 assertions, 0 failures, 0 errors" vcr-5.0.0/lib/000077500000000000000000000000001347305653000130625ustar00rootroot00000000000000vcr-5.0.0/lib/vcr.rb000066400000000000000000000340321347305653000142030ustar00rootroot00000000000000require '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 cassete'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 :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) ensure eject_cassette end end # Inserts multiple cassettes the given names # # @example # cassettes = [ # { name: 'github' }, # { name: 'apple', options: { erb: true } } # ] # VCR.use_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? 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, if it has previously been turned off. # @return [void] # @see #turn_off! # @see #turned_off # @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? !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 vcr-5.0.0/lib/vcr/000077500000000000000000000000001347305653000136545ustar00rootroot00000000000000vcr-5.0.0/lib/vcr/cassette.rb000066400000000000000000000254411347305653000160220ustar00rootroot00000000000000require 'vcr/cassette/http_interaction_list' require 'vcr/cassette/erb_renderer' require 'vcr/cassette/serializers' module VCR # The media VCR uses to store HTTP interactions for later re-use. class Cassette include Logger::Mixin # The supported record modes. # # * :all -- Record every HTTP interactions; do not play any back. # * :none -- Do not record any HTTP interactions; play them back. # * :new_episodes -- Playback previously recorded HTTP interactions and record new ones. # * :once -- Record the HTTP interactions if the cassette has not already been recorded; # otherwise, playback the HTTP interactions. VALID_RECORD_MODES = [:all, :none, :new_episodes, :once] # @return [#to_s] The name of the cassette. Used to determine the cassette's file name. # @see #file attr_reader :name # @return [Symbol] The record mode. Determines whether the cassette records HTTP interactions, # plays them back, or does both. attr_reader :record_mode # @return [Array] List of request matchers. Used to find a response from an # existing HTTP interaction to play back. attr_reader :match_requests_on # @return [Boolean, Hash] The cassette's ERB option. The file will be treated as an # ERB template if this has a truthy value. A hash, if provided, will be used as local # variables for the ERB template. attr_reader :erb # @return [Integer, nil] How frequently (in seconds) the cassette should be re-recorded. attr_reader :re_record_interval # @return [Boolean, nil] Should outdated interactions be recorded back to file attr_reader :clean_outdated_http_interactions # @return [Array] If set, {VCR::Configuration#before_record} and # {VCR::Configuration#before_playback} hooks with a corresponding tag will apply. attr_reader :tags # @param (see VCR#insert_cassette) # @see VCR#insert_cassette def initialize(name, options = {}) @name = name @options = VCR.configuration.default_cassette_options.merge(options) @mutex = Mutex.new assert_valid_options! extract_options raise_error_unless_valid_record_mode log "Initialized with options: #{@options.inspect}" end # Ejects the current cassette. The cassette will no longer be used. # In addition, any newly recorded HTTP interactions will be written to # disk. # # @note This is not intended to be called directly. Use `VCR.eject_cassette` instead. # # @param (see VCR#eject_casssette) # @see VCR#eject_cassette def eject(options = {}) write_recorded_interactions_to_disk if should_assert_no_unused_interactions? && !options[:skip_no_unused_interactions_assertion] http_interactions.assert_no_unused_interactions! end end # @private def http_interactions # Without this mutex, under threaded access, an HTTPInteractionList will overwrite # the first. @mutex.synchronize do @http_interactions ||= HTTPInteractionList.new \ should_stub_requests? ? previously_recorded_interactions : [], match_requests_on, @allow_playback_repeats, @parent_list, log_prefix end end # @private def record_http_interaction(interaction) VCR::CassetteMutex.synchronize do log "Recorded HTTP interaction #{request_summary(interaction.request)} => #{response_summary(interaction.response)}" new_recorded_interactions << interaction end end # @private def new_recorded_interactions @new_recorded_interactions ||= [] end # @return [String] The file for this cassette. # @raise [NotImplementedError] if the configured cassette persister # does not support resolving file paths. # @note VCR will take care of sanitizing the cassette name to make it a valid file name. def file unless @persister.respond_to?(:absolute_path_to_file) raise NotImplementedError, "The configured cassette persister does not support resolving file paths" end @persister.absolute_path_to_file(storage_key) end # @return [Boolean] Whether or not the cassette is recording. def recording? case record_mode when :none; false when :once; raw_cassette_bytes.to_s.empty? else true end end # @return [Hash] The hash that will be serialized when the cassette is written to disk. def serializable_hash { "http_interactions" => interactions_to_record.map(&:to_hash), "recorded_with" => "VCR #{VCR.version}" } end # @return [Time, nil] The `recorded_at` time of the first HTTP interaction # or nil if the cassette has no prior HTTP interactions. # # @example # # VCR.use_cassette("some cassette") do |cassette| # Timecop.freeze(cassette.originally_recorded_at || Time.now) do # # ... # end # end def originally_recorded_at @originally_recorded_at ||= previously_recorded_interactions.map(&:recorded_at).min end # @return [Boolean] false unless wrapped with LinkedCassette def linked? false end private def assert_valid_options! invalid_options = @options.keys - [ :record, :erb, :match_requests_on, :re_record_interval, :tag, :tags, :update_content_length_header, :allow_playback_repeats, :allow_unused_http_interactions, :exclusive, :serialize_with, :preserve_exact_body_bytes, :decode_compressed_response, :recompress_response, :persist_with, :clean_outdated_http_interactions ] if invalid_options.size > 0 raise ArgumentError.new("You passed the following invalid options to VCR::Cassette.new: #{invalid_options.inspect}.") end end def extract_options [:erb, :match_requests_on, :re_record_interval, :clean_outdated_http_interactions, :allow_playback_repeats, :allow_unused_http_interactions, :exclusive].each do |name| instance_variable_set("@#{name}", @options[name]) end assign_tags @record_mode = @options[:record] @serializer = VCR.cassette_serializers[@options[:serialize_with]] @persister = VCR.cassette_persisters[@options[:persist_with]] @record_mode = :all if should_re_record? @parent_list = @exclusive ? HTTPInteractionList::NullList : VCR.http_interactions end def assign_tags @tags = Array(@options.fetch(:tags) { @options[:tag] }) [:update_content_length_header, :preserve_exact_body_bytes, :decode_compressed_response, :recompress_response].each do |tag| @tags << tag if @options[tag] end end def previously_recorded_interactions @previously_recorded_interactions ||= if !raw_cassette_bytes.to_s.empty? deserialized_hash['http_interactions'].map { |h| HTTPInteraction.from_hash(h) }.tap do |interactions| invoke_hook(:before_playback, interactions) interactions.reject! do |i| i.request.uri.is_a?(String) && VCR.request_ignorer.ignore?(i.request) end end else [] end end def storage_key @storage_key ||= [name, @serializer.file_extension].join('.') end def raise_error_unless_valid_record_mode unless VALID_RECORD_MODES.include?(record_mode) raise ArgumentError.new("#{record_mode} is not a valid cassette record mode. Valid modes are: #{VALID_RECORD_MODES.inspect}") end end def should_re_record? return false unless @re_record_interval return false unless originally_recorded_at now = Time.now (originally_recorded_at + @re_record_interval < now).tap do |value| info = "previously recorded at: '#{originally_recorded_at}'; now: '#{now}'; interval: #{@re_record_interval} seconds" if !value log "Not re-recording since the interval has not elapsed (#{info})." elsif InternetConnection.available? log "re-recording (#{info})." else log "Not re-recording because no internet connection is available (#{info})." return false end end end def should_stub_requests? record_mode != :all end def should_remove_matching_existing_interactions? record_mode == :all end def should_assert_no_unused_interactions? !(@allow_unused_http_interactions || $!) end def raw_cassette_bytes @raw_cassette_bytes ||= VCR::Cassette::ERBRenderer.new(@persister[storage_key], erb, name).render end def merged_interactions old_interactions = previously_recorded_interactions if should_remove_matching_existing_interactions? new_interaction_list = HTTPInteractionList.new(new_recorded_interactions, match_requests_on) old_interactions = old_interactions.reject do |i| new_interaction_list.response_for(i.request) end end up_to_date_interactions(old_interactions) + new_recorded_interactions end def up_to_date_interactions(interactions) return interactions unless clean_outdated_http_interactions && re_record_interval interactions.take_while { |x| x[:recorded_at] > Time.now - re_record_interval } end def interactions_to_record # We deep-dup the interactions by roundtripping them to/from a hash. # This is necessary because `before_record` can mutate the interactions. merged_interactions.map { |i| HTTPInteraction.from_hash(i.to_hash) }.tap do |interactions| invoke_hook(:before_record, interactions) end end def write_recorded_interactions_to_disk return if new_recorded_interactions.none? hash = serializable_hash return if hash["http_interactions"].none? @persister[storage_key] = @serializer.serialize(hash) end def invoke_hook(type, interactions) interactions.delete_if do |i| i.hook_aware.tap do |hw| VCR.configuration.invoke_hook(type, hw, self) end.ignored? end end def deserialized_hash @deserialized_hash ||= @serializer.deserialize(raw_cassette_bytes).tap do |hash| unless hash.is_a?(Hash) && hash['http_interactions'].is_a?(Array) raise Errors::InvalidCassetteFormatError.new \ "#{file} does not appear to be a valid VCR 2.0 cassette. " + "VCR 1.x cassettes are not valid with VCR 2.0. When upgrading from " + "VCR 1.x, it is recommended that you delete all your existing cassettes and " + "re-record them, or use the provided vcr:migrate_cassettes rake task to migrate " + "them. For more info, see the VCR upgrade guide." end end end def log_prefix @log_prefix ||= "[Cassette: '#{name}'] " end def request_summary(request) super(request, match_requests_on) end end end vcr-5.0.0/lib/vcr/cassette/000077500000000000000000000000001347305653000154675ustar00rootroot00000000000000vcr-5.0.0/lib/vcr/cassette/erb_renderer.rb000066400000000000000000000026051347305653000204550ustar00rootroot00000000000000require 'erb' module VCR class Cassette # @private class ERBRenderer def initialize(raw_template, erb, cassette_name=nil) @raw_template, @erb, @cassette_name = raw_template, erb, cassette_name end def render return @raw_template if @raw_template.nil? || !use_erb? binding = binding_for_variables if erb_variables template.result(binding) rescue NameError => e handle_name_error(e) end private def handle_name_error(e) example_hash = (erb_variables || {}).merge(e.name => 'some value') raise Errors::MissingERBVariableError.new( "The ERB in the #{@cassette_name} cassette file references undefined variable #{e.name}. " + "Pass it to the cassette using :erb => #{ example_hash.inspect }." ) end def use_erb? !!@erb end def erb_variables @erb if @erb.is_a?(Hash) end def template @template ||= ERB.new(@raw_template) end @@struct_cache = Hash.new do |hash, attributes| hash[attributes] = Struct.new(*attributes) end def variables_object @variables_object ||= @@struct_cache[erb_variables.keys].new(*erb_variables.values) end def binding_for_variables @binding_for_variables ||= variables_object.instance_eval { binding } end end end end vcr-5.0.0/lib/vcr/cassette/http_interaction_list.rb000066400000000000000000000103701347305653000224260ustar00rootroot00000000000000module VCR class Cassette # @private class HTTPInteractionList include Logger::Mixin # @private module NullList extend self def response_for(*a); nil; end def has_interaction_matching?(*a); false; end def has_used_interaction_matching?(*a); false; end def remaining_unused_interaction_count(*a); 0; end end attr_reader :interactions, :request_matchers, :allow_playback_repeats, :parent_list def initialize(interactions, request_matchers, allow_playback_repeats = false, parent_list = NullList, log_prefix = '') @interactions = interactions.dup @request_matchers = request_matchers @allow_playback_repeats = allow_playback_repeats @parent_list = parent_list @used_interactions = [] @log_prefix = log_prefix @mutex = Mutex.new interaction_summaries = interactions.map { |i| "#{request_summary(i.request)} => #{response_summary(i.response)}" } log "Initialized HTTPInteractionList with request matchers #{request_matchers.inspect} and #{interactions.size} interaction(s): { #{interaction_summaries.join(', ')} }", 1 end def response_for(request) # Without this mutex, under threaded access, the wrong response may be removed # out of the (remaining) interactions list (and other problems). @mutex.synchronize do if index = matching_interaction_index_for(request) interaction = @interactions.delete_at(index) @used_interactions.unshift interaction log "Found matching interaction for #{request_summary(request)} at index #{index}: #{response_summary(interaction.response)}", 1 interaction.response elsif interaction = matching_used_interaction_for(request) interaction.response else @parent_list.response_for(request) end end end def has_interaction_matching?(request) !!matching_interaction_index_for(request) || !!matching_used_interaction_for(request) || @parent_list.has_interaction_matching?(request) end def has_used_interaction_matching?(request) @used_interactions.any? { |i| interaction_matches_request?(request, i) } end def remaining_unused_interaction_count @interactions.size end # Checks if there are no unused interactions left. # # @raise [VCR::Errors::UnusedHTTPInteractionError] if not all interactions were played back. def assert_no_unused_interactions! return unless has_unused_interactions? logger = Logger.new(nil) descriptions = @interactions.map do |i| " - #{logger.request_summary(i.request, @request_matchers)} => #{logger.response_summary(i.response)}" end.join("\n") raise Errors::UnusedHTTPInteractionError, "There are unused HTTP interactions left in the cassette:\n#{descriptions}" end private # @return [Boolean] Whether or not there are unused interactions left in the list. def has_unused_interactions? @interactions.size > 0 end def request_summary(request) super(request, @request_matchers) end def matching_interaction_index_for(request) @interactions.index { |i| interaction_matches_request?(request, i) } end def matching_used_interaction_for(request) return nil unless @allow_playback_repeats @used_interactions.find { |i| interaction_matches_request?(request, i) } end def interaction_matches_request?(request, interaction) log "Checking if #{request_summary(request)} matches #{request_summary(interaction.request)} using #{@request_matchers.inspect}", 1 @request_matchers.all? do |matcher_name| matcher = VCR.request_matchers[matcher_name] matcher.matches?(request, interaction.request).tap do |matched| matched = matched ? 'matched' : 'did not match' log "#{matcher_name} (#{matched}): current request #{request_summary(request)} vs #{request_summary(interaction.request)}", 2 end end end def log_prefix @log_prefix end end end end vcr-5.0.0/lib/vcr/cassette/migrator.rb000066400000000000000000000064701347305653000176470ustar00rootroot00000000000000require 'vcr' module VCR class Cassette # @private class Migrator def initialize(dir, out = $stdout) @dir, @out = dir, out @yaml_load_errors = yaml_load_errors end def migrate! @out.puts "Migrating VCR cassettes in #{@dir}..." Dir["#{@dir}/**/*.yml"].each do |cassette| migrate_cassette(cassette) end end private def migrate_cassette(cassette) unless http_interactions = load_yaml(cassette) @out.puts " - Ignored #{relative_casssette_name(cassette)} since it could not be parsed as YAML (does it have some ERB?)" return end unless valid_vcr_1_cassette?(http_interactions) @out.puts " - Ignored #{relative_casssette_name(cassette)} since it does not appear to be a valid VCR 1.x cassette" return end http_interactions.map! do |interaction| interaction.response.adapter_metadata = {} interaction.recorded_at = File.mtime(cassette) remove_unnecessary_standard_port(interaction) denormalize_http_header_keys(interaction.request) denormalize_http_header_keys(interaction.response) normalize_body(interaction.request) normalize_body(interaction.response) interaction.to_hash end hash = { "http_interactions" => http_interactions, "recorded_with" => "VCR #{VCR.version}" } File.open(cassette, 'w') { |f| f.write ::YAML.dump(hash) } @out.puts " - Migrated #{relative_casssette_name(cassette)}" end def load_yaml(cassette) ::YAML.load_file(cassette) rescue *@yaml_load_errors return nil end def yaml_load_errors [ArgumentError].tap do |errors| errors << Psych::SyntaxError if defined?(Psych::SyntaxError) end end def relative_casssette_name(cassette) cassette.gsub(%r|\A#{Regexp.escape(@dir)}/?|, '') end def valid_vcr_1_cassette?(content) content.is_a?(Array) && content.map(&:class).uniq == [HTTPInteraction] end def remove_unnecessary_standard_port(interaction) uri = VCR.configuration.uri_parser.parse(interaction.request.uri) if uri.scheme == 'http' && uri.port == 80 || uri.scheme == 'https' && uri.port == 443 uri.port = nil interaction.request.uri = uri.to_s end rescue URI::InvalidURIError # ignore this URI. # This can occur when the user uses the filter_sensitive_data option # to put a substitution string in their URI end def denormalize_http_header_keys(object) object.headers = {}.tap do |denormalized| object.headers.each do |k, v| denormalized[denormalize_header_key(k)] = v end if object.headers end end def denormalize_header_key(key) key.split('-'). # 'user-agent' => %w(user agent) each { |w| w.capitalize! }. # => %w(User Agent) join('-') end EMPTY_STRING = if String.method_defined?(:force_encoding) ''.force_encoding("US-ASCII") else '' end def normalize_body(object) object.body = EMPTY_STRING if object.body.nil? end end end end vcr-5.0.0/lib/vcr/cassette/persisters.rb000066400000000000000000000023621347305653000202220ustar00rootroot00000000000000module VCR class Cassette # Keeps track of the cassette persisters in a hash-like object. class Persisters autoload :FileSystem, 'vcr/cassette/persisters/file_system' # @private def initialize @persisters = {} end # Gets the named persister. # # @param name [Symbol] the name of the persister # @return the named persister # @raise [ArgumentError] if there is not a persister for the given name def [](name) @persisters.fetch(name) do |_| @persisters[name] = case name when :file_system then FileSystem else raise ArgumentError, "The requested VCR cassette persister " + "(#{name.inspect}) is not registered." end end end # Registers a persister. # # @param name [Symbol] the name of the persister # @param value [#[], #[]=] the persister object. It must implement `[]` and `[]=`. def []=(name, value) if @persisters.has_key?(name) warn "WARNING: There is already a VCR cassette persister " + "registered for #{name.inspect}. Overriding it." end @persisters[name] = value end end end end vcr-5.0.0/lib/vcr/cassette/persisters/000077500000000000000000000000001347305653000176725ustar00rootroot00000000000000vcr-5.0.0/lib/vcr/cassette/persisters/file_system.rb000066400000000000000000000034551347305653000225510ustar00rootroot00000000000000require 'fileutils' module VCR class Cassette class Persisters # The only built-in cassette persister. Persists cassettes to the file system. module FileSystem extend self # @private attr_reader :storage_location # @private def storage_location=(dir) FileUtils.mkdir_p(dir) if dir @storage_location = dir ? absolute_path_for(dir) : nil end # Gets the cassette for the given storage key (file name). # # @param [String] file_name the file name # @return [String] the cassette content def [](file_name) path = absolute_path_to_file(file_name) return nil unless File.exist?(path) File.binread(path) end # Sets the cassette for the given storage key (file name). # # @param [String] file_name the file name # @param [String] content the content to store def []=(file_name, content) path = absolute_path_to_file(file_name) directory = File.dirname(path) FileUtils.mkdir_p(directory) unless File.exist?(directory) File.binwrite(path, content) end # @private def absolute_path_to_file(file_name) return nil unless storage_location File.join(storage_location, sanitized_file_name_from(file_name)) end private def absolute_path_for(path) Dir.chdir(path) { Dir.pwd } end def sanitized_file_name_from(file_name) parts = file_name.to_s.split('.') if parts.size > 1 && !parts.last.include?(File::SEPARATOR) file_extension = '.' + parts.pop end parts.join('.').gsub(/[^[:word:]\-\/]+/, '_') + file_extension.to_s end end end end end vcr-5.0.0/lib/vcr/cassette/serializers.rb000066400000000000000000000037261347305653000203600ustar00rootroot00000000000000module VCR class Cassette # Keeps track of the cassette serializers in a hash-like object. class Serializers autoload :YAML, 'vcr/cassette/serializers/yaml' autoload :Syck, 'vcr/cassette/serializers/syck' autoload :Psych, 'vcr/cassette/serializers/psych' autoload :JSON, 'vcr/cassette/serializers/json' autoload :Compressed, 'vcr/cassette/serializers/compressed' # @private def initialize @serializers = {} end # Gets the named serializer. # # @param name [Symbol] the name of the serializer # @return the named serializer # @raise [ArgumentError] if there is not a serializer for the given name def [](name) @serializers.fetch(name) do |_| @serializers[name] = case name when :yaml then YAML when :syck then Syck when :psych then Psych when :json then JSON when :compressed then Compressed else raise ArgumentError.new("The requested VCR cassette serializer (#{name.inspect}) is not registered.") end end end # Registers a serializer. # # @param name [Symbol] the name of the serializer # @param value [#file_extension, #serialize, #deserialize] the serializer object. It must implement # `file_extension()`, `serialize(Hash)` and `deserialize(String)`. def []=(name, value) if @serializers.has_key?(name) warn "WARNING: There is already a VCR cassette serializer registered for #{name.inspect}. Overriding it." end @serializers[name] = value end end # @private module EncodingErrorHandling def handle_encoding_errors yield rescue *self::ENCODING_ERRORS => e e.message << "\nNote: Using VCR's `:preserve_exact_body_bytes` option may help prevent this error in the future." raise end end end end vcr-5.0.0/lib/vcr/cassette/serializers/000077500000000000000000000000001347305653000200235ustar00rootroot00000000000000vcr-5.0.0/lib/vcr/cassette/serializers/compressed.rb000066400000000000000000000025011347305653000225120ustar00rootroot00000000000000require 'zlib' require 'vcr/cassette/serializers' module VCR class Cassette class Serializers # The compressed serializer. This serializer wraps the YAML serializer # to write compressed cassettes to disk. # # Cassettes containing responses with JSON data often compress at greater # than 10:1. The tradeoff is that cassettes will not diff nicely or be # easily inspectable or editable. # # @see YAML module Compressed extend self # The file extension to use for this serializer. # # @return [String] "zz" def file_extension 'zz' end # Serializes the given hash using YAML and Zlib. # # @param [Hash] hash the object to serialize # @return [String] the compressed cassette data def serialize(hash) string = VCR::Cassette::Serializers::YAML.serialize(hash) Zlib::Deflate.deflate(string) end # Deserializes the given compressed cassette data. # # @param [String] string the compressed YAML cassette data # @return [Hash] the deserialized object def deserialize(string) yaml = Zlib::Inflate.inflate(string) VCR::Cassette::Serializers::YAML.deserialize(yaml) end end end end end vcr-5.0.0/lib/vcr/cassette/serializers/json.rb000066400000000000000000000022601347305653000213210ustar00rootroot00000000000000require 'multi_json' module VCR class Cassette class Serializers # The JSON serializer. Uses `MultiJson` under the covers. # # @see Psych # @see Syck # @see YAML module JSON extend self extend EncodingErrorHandling # @private ENCODING_ERRORS = [MultiJson::DecodeError, ArgumentError] ENCODING_ERRORS << EncodingError if defined?(EncodingError) # The file extension to use for this serializer. # # @return [String] "json" def file_extension "json" end # Serializes the given hash using `MultiJson`. # # @param [Hash] hash the object to serialize # @return [String] the JSON string def serialize(hash) handle_encoding_errors do MultiJson.encode(hash) end end # Deserializes the given string using `MultiJson`. # # @param [String] string the JSON string # @return [Hash] the deserialized object def deserialize(string) handle_encoding_errors do MultiJson.decode(string) end end end end end end vcr-5.0.0/lib/vcr/cassette/serializers/psych.rb000066400000000000000000000021021347305653000214710ustar00rootroot00000000000000require 'psych' module VCR class Cassette class Serializers # The Psych serializer. Psych is the new YAML engine in ruby 1.9. # # @see JSON # @see Syck # @see YAML module Psych extend self extend EncodingErrorHandling # @private ENCODING_ERRORS = [ArgumentError] # The file extension to use for this serializer. # # @return [String] "yml" def file_extension "yml" end # Serializes the given hash using Psych. # # @param [Hash] hash the object to serialize # @return [String] the YAML string def serialize(hash) handle_encoding_errors do ::Psych.dump(hash) end end # Deserializes the given string using Psych. # # @param [String] string the YAML string # @return [Hash] the deserialized object def deserialize(string) handle_encoding_errors do ::Psych.load(string) end end end end end end vcr-5.0.0/lib/vcr/cassette/serializers/syck.rb000066400000000000000000000026361347305653000213300ustar00rootroot00000000000000require 'yaml' module VCR class Cassette class Serializers # The Syck serializer. Syck is the legacy YAML engine in ruby 1.8 and 1.9. # # @see JSON # @see Psych # @see YAML module Syck extend self extend EncodingErrorHandling # @private ENCODING_ERRORS = [ArgumentError] # The file extension to use for this serializer. # # @return [String] "yml" def file_extension "yml" end # Serializes the given hash using Syck. # # @param [Hash] hash the object to serialize # @return [String] the YAML string def serialize(hash) handle_encoding_errors do using_syck { ::YAML.dump(hash) } end end # Deserializes the given string using Syck. # # @param [String] string the YAML string # @return [Hash] the deserialized object def deserialize(string) handle_encoding_errors do using_syck { ::YAML.load(string) } end end private def using_syck return yield unless defined?(::YAML::ENGINE) original_engine = ::YAML::ENGINE.yamler ::YAML::ENGINE.yamler = 'syck' begin yield ensure ::YAML::ENGINE.yamler = original_engine end end end end end end vcr-5.0.0/lib/vcr/cassette/serializers/yaml.rb000066400000000000000000000023031347305653000213100ustar00rootroot00000000000000require 'yaml' module VCR class Cassette class Serializers # The YAML serializer. This will use either Psych or Syck, which ever your # ruby interpreter defaults to. You can also force VCR to use Psych or Syck by # using one of those serializers. # # @see JSON # @see Psych # @see Syck module YAML extend self extend EncodingErrorHandling # @private ENCODING_ERRORS = [ArgumentError] # The file extension to use for this serializer. # # @return [String] "yml" def file_extension "yml" end # Serializes the given hash using YAML. # # @param [Hash] hash the object to serialize # @return [String] the YAML string def serialize(hash) handle_encoding_errors do ::YAML.dump(hash) end end # Deserializes the given string using YAML. # # @param [String] string the YAML string # @return [Hash] the deserialized object def deserialize(string) handle_encoding_errors do ::YAML.load(string) end end end end end end vcr-5.0.0/lib/vcr/configuration.rb000066400000000000000000000526361347305653000170640ustar00rootroot00000000000000require '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] 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] 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] 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 "" in place of the actual API key in # # our cassettes so we don't have to commit to source control. # c.filter_sensitive_data('') { GithubClient.api_key } # # # Put a "" placeholder variable in our cassettes tagged with # # :user_cassette since it can be different for different test runs. # c.define_cassette_placeholder('', :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 vcr-5.0.0/lib/vcr/deprecations.rb000066400000000000000000000025651347305653000166710ustar00rootroot00000000000000module VCR # @deprecated Use #configure instead. # @see #configure def config warn "WARNING: `VCR.config` is deprecated. Use VCR.configure instead." configure { |c| yield c } end # @private def self.const_missing(const) return super unless const == :Config warn "WARNING: `VCR::Config` is deprecated. Use VCR.configuration instead." configuration end # @private def Cassette.const_missing(const) return super unless const == :MissingERBVariableError warn "WARNING: `VCR::Cassette::MissingERBVariableError` is deprecated. Use `VCR::Errors::MissingERBVariableError` instead." Errors::MissingERBVariableError end class Configuration # @deprecated Use #hook_into instead. # @see #hook_into def stub_with(*adapters) warn "WARNING: `VCR.configure { |c| c.stub_with ... }` is deprecated. Use `VCR.configure { |c| c.hook_into ... }` instead." hook_into(*adapters) end end # @private module Deprecations module Middleware # @private module Faraday def initialize(*args) if block_given? Kernel.warn "WARNING: Passing a block to `VCR::Middleware::Faraday` is deprecated. \n" + "As of VCR 2.0, you need to wrap faraday requests in VCR.use_cassette, just like with any other library hook." end end end end end end vcr-5.0.0/lib/vcr/errors.rb000066400000000000000000000264221347305653000155230ustar00rootroot00000000000000module VCR # Namespace for VCR errors. module Errors # Base class for all VCR errors. class Error < StandardError; end # Error raised when VCR is turned off while a cassette is in use. # @see VCR#turn_off! # @see VCR#turned_off class CassetteInUseError < Error; end # Error raised when a VCR cassette is inserted while VCR is turned off. # @see VCR#insert_cassette # @see VCR#use_cassette class TurnedOffError < Error; end # Error raised when an cassette ERB template is rendered and a # variable is missing. # @see VCR#insert_cassette # @see VCR#use_cassette class MissingERBVariableError < Error; end # Error raised when the version of one of the libraries that VCR hooks into # is too low for VCR to support. # @see VCR::Configuration#hook_into class LibraryVersionTooLowError < Error; end # Error raised when a request matcher is requested that is not registered. class UnregisteredMatcherError < Error; end # Error raised when a VCR 1.x cassette is used with VCR 2. class InvalidCassetteFormatError < Error; end # Error raised when an `around_http_request` hook is used improperly. # @see VCR::Configuration#around_http_request class AroundHTTPRequestHookError < Error; end # Error raised when you attempt to use a VCR feature that is not # supported on your ruby interpreter. # @see VCR::Configuration#around_http_request class NotSupportedError < Error; end # Error raised when you ask VCR to decode a compressed response # body but the content encoding isn't one of the known ones. # @see VCR::Response#decompress class UnknownContentEncodingError < Error; end # Error raised when you eject a cassette before all previously # recorded HTTP interactions are played back. # @note Only applicable when :allow_episode_skipping is false. # @see VCR::HTTPInteractionList#assert_no_unused_interactions! class UnusedHTTPInteractionError < Error; end # Error raised when you attempt to eject a cassette inserted by another # thread. class EjectLinkedCassetteError < Error; end # Error raised when an HTTP request is made that VCR is unable to handle. # @note VCR will raise this to force you to do something about the # HTTP request. The idea is that you want to handle _every_ HTTP # request in your test suite. The error message will give you # suggestions for how to deal with the request. class UnhandledHTTPRequestError < Error # The HTTP request. attr_reader :request # Constructs the error. # # @param [VCR::Request] request the unhandled request. def initialize(request) @request = request super construct_message end private def relish_version_slug @relish_version_slug ||= VCR.version.gsub(/\W/, '-') end def construct_message ["", "", "=" * 80, "An HTTP request has been made that VCR does not know how to handle:", "#{request_description}\n", cassettes_description, formatted_suggestions, "=" * 80, "", ""].join("\n") end def current_cassettes @cassettes ||= VCR.cassettes.to_a.reverse end def request_description lines = [] lines << " #{request.method.to_s.upcase} #{request.uri}" if match_request_on_headers? lines << " Headers:\n#{formatted_headers}" end if match_request_on_body? lines << " Body: #{request.body}" end lines.join("\n") end def match_request_on_headers? current_matchers.include?(:headers) end def match_request_on_body? current_matchers.include?(:body) end def current_matchers if current_cassettes.size > 0 current_cassettes.inject([]) do |memo, cassette| memo | cassette.match_requests_on end else VCR.configuration.default_cassette_options[:match_requests_on] end end def formatted_headers request.headers.flat_map do |header, values| values.map do |val| " #{header}: #{val.inspect}" end end.join("\n") end def cassettes_description if current_cassettes.size > 0 [cassettes_list << "\n", "Under the current configuration VCR can not find a suitable HTTP interaction", "to replay and is prevented from recording new requests. There are a few ways", "you can deal with this:\n"].join("\n") else ["There is currently no cassette in use. There are a few ways", "you can configure VCR to handle this request:\n"].join("\n") end end def cassettes_list lines = [] lines << if current_cassettes.size == 1 "VCR is currently using the following cassette:" else "VCR are currently using the following cassettes:" end lines = current_cassettes.inject(lines) do |memo, cassette| memo.concat([ " - #{cassette.file}", " - :record => #{cassette.record_mode.inspect}", " - :match_requests_on => #{cassette.match_requests_on.inspect}" ]) end lines.join("\n") end def formatted_suggestions formatted_points, formatted_foot_notes = [], [] suggestions.each_with_index do |suggestion, index| bullet_point, foot_note = suggestion.first, suggestion.last formatted_points << format_bullet_point(bullet_point, index) formatted_foot_notes << format_foot_note(foot_note, index) end [ formatted_points.join("\n"), formatted_foot_notes.join("\n") ].join("\n\n") end def format_bullet_point(lines, index) lines.first.insert(0, " * ") lines.last << " [#{index + 1}]." lines.join("\n ") end def format_foot_note(url, index) "[#{index + 1}] #{url % relish_version_slug}" end # List of suggestions for how to configure VCR to handle the request. ALL_SUGGESTIONS = { :use_new_episodes => [ ["You can use the :new_episodes record mode to allow VCR to", "record this new request to the existing cassette"], "https://www.relishapp.com/vcr/vcr/v/%s/docs/record-modes/new-episodes" ], :delete_cassette_for_once => [ ["The current record mode (:once) does not allow new requests to be recorded", "to a previously recorded cassette. You can delete the cassette file and re-run", "your tests to allow the cassette to be recorded with this request"], "https://www.relishapp.com/vcr/vcr/v/%s/docs/record-modes/once" ], :deal_with_none => [ ["The current record mode (:none) does not allow requests to be recorded. You", "can temporarily change the record mode to :once, delete the cassette file ", "and re-run your tests to allow the cassette to be recorded with this request"], "https://www.relishapp.com/vcr/vcr/v/%s/docs/record-modes/none" ], :use_a_cassette => [ ["If you want VCR to record this request and play it back during future test", "runs, you should wrap your test (or this portion of your test) in a", "`VCR.use_cassette` block"], "https://www.relishapp.com/vcr/vcr/v/%s/docs/getting-started" ], :allow_http_connections_when_no_cassette => [ ["If you only want VCR to handle requests made while a cassette is in use,", "configure `allow_http_connections_when_no_cassette = true`. VCR will", "ignore this request since it is made when there is no cassette"], "https://www.relishapp.com/vcr/vcr/v/%s/docs/configuration/allow-http-connections-when-no-cassette" ], :ignore_request => [ ["If you want VCR to ignore this request (and others like it), you can", "set an `ignore_request` callback"], "https://www.relishapp.com/vcr/vcr/v/%s/docs/configuration/ignore-request" ], :allow_playback_repeats => [ ["The cassette contains an HTTP interaction that matches this request,", "but it has already been played back. If you wish to allow a single HTTP", "interaction to be played back multiple times, set the `:allow_playback_repeats`", "cassette option"], "https://www.relishapp.com/vcr/vcr/v/%s/docs/request-matching/playback-repeats" ], :match_requests_on => [ ["The cassette contains %s not been", "played back. If your request is non-deterministic, you may need to", "change your :match_requests_on cassette option to be more lenient", "or use a custom request matcher to allow it to match"], "https://www.relishapp.com/vcr/vcr/v/%s/docs/request-matching" ], :try_debug_logger => [ ["If you're surprised VCR is raising this error", "and want insight about how VCR attempted to handle the request,", "you can use the debug_logger configuration option to log more details"], "https://www.relishapp.com/vcr/vcr/v/%s/docs/configuration/debug-logging" ] } def suggestion_for(key) bullet_point_lines, url = ALL_SUGGESTIONS[key] bullet_point_lines = bullet_point_lines.map(&:dup) url = url.dup [bullet_point_lines, url] end def suggestions return no_cassette_suggestions if current_cassettes.size == 0 [:try_debug_logger, :use_new_episodes, :ignore_request].tap do |suggestions| suggestions.push(*record_mode_suggestion) suggestions << :allow_playback_repeats if has_used_interaction_matching? suggestions.map! { |k| suggestion_for(k) } suggestions.push(*match_requests_on_suggestion) end end def no_cassette_suggestions [:try_debug_logger, :use_a_cassette, :allow_http_connections_when_no_cassette, :ignore_request].map do |key| suggestion_for(key) end end def record_mode_suggestion record_modes = current_cassettes.map(&:record_mode) if record_modes.all?{|r| r == :none } [:deal_with_none] elsif record_modes.all?{|r| r == :once } [:delete_cassette_for_once] else [] end end def has_used_interaction_matching? current_cassettes.any?{|c| c.http_interactions.has_used_interaction_matching?(request) } end def match_requests_on_suggestion num_remaining_interactions = current_cassettes.inject(0) { |sum, c| sum + c.http_interactions.remaining_unused_interaction_count } return [] if num_remaining_interactions.zero? interaction_description = if num_remaining_interactions == 1 "1 HTTP interaction that has" else "#{num_remaining_interactions} HTTP interactions that have" end description_lines, link = suggestion_for(:match_requests_on) description_lines[0] = description_lines[0] % interaction_description [[description_lines, link]] end end end end vcr-5.0.0/lib/vcr/library_hooks.rb000066400000000000000000000004521347305653000170510ustar00rootroot00000000000000module VCR # @private class LibraryHooks attr_accessor :exclusive_hook def disabled?(hook) ![nil, hook].include?(exclusive_hook) end def exclusively_enabled(hook) self.exclusive_hook = hook yield ensure self.exclusive_hook = nil end end end vcr-5.0.0/lib/vcr/library_hooks/000077500000000000000000000000001347305653000165235ustar00rootroot00000000000000vcr-5.0.0/lib/vcr/library_hooks/excon.rb000066400000000000000000000016531347305653000201710ustar00rootroot00000000000000require 'vcr/middleware/excon' module VCR class LibraryHooks module Excon # @private def self.configure_middleware middlewares = ::Excon.defaults[:middlewares] middlewares << VCR::Middleware::Excon::Request response_parser_index = middlewares.index(::Excon::Middleware::ResponseParser) middlewares.insert(response_parser_index + 1, VCR::Middleware::Excon::Response) end configure_middleware end end end VCR.configuration.after_library_hooks_loaded do # ensure WebMock's Excon adapter does not conflict with us here # (i.e. to double record requests or whatever). if defined?(WebMock::HttpLibAdapters::ExconAdapter) WebMock::HttpLibAdapters::ExconAdapter.disable! if defined?(::RSpec) ::RSpec.configure do |config| config.before(:suite) do WebMock::HttpLibAdapters::ExconAdapter.disable! end end end end end vcr-5.0.0/lib/vcr/library_hooks/faraday.rb000066400000000000000000000030731347305653000204620ustar00rootroot00000000000000require 'vcr/middleware/faraday' module VCR class LibraryHooks # @private module Faraday # @private module BuilderClassExtension def new(*args) super.extend BuilderInstanceExtension end if defined?(::Faraday::RackBuilder) ::Faraday::RackBuilder.extend self else ::Faraday::Builder.extend self end end # @private module BuilderInstanceExtension def lock!(*args) insert_vcr_middleware super end private def insert_vcr_middleware return if handlers.any? { |h| h.klass == VCR::Middleware::Faraday } adapter_index = handlers.index { |h| h.klass < ::Faraday::Adapter } adapter_index ||= handlers.size warn_about_after_adapter_middleware(adapter_index) insert_before(adapter_index, VCR::Middleware::Faraday) end def warn_about_after_adapter_middleware(adapter_index) after_adapter_middleware_count = (handlers.size - adapter_index - 1) return if after_adapter_middleware_count < 1 after_adapter_middlewares = handlers.last(after_adapter_middleware_count) warn "WARNING: The Faraday connection stack contains middleware after " + "the HTTP adapter (#{after_adapter_middlewares.map(&:inspect).join(', ')}). " + "This is a non-standard configuration and VCR may not be able to " + "record the HTTP requests made through this Faraday connection." end end end end end vcr-5.0.0/lib/vcr/library_hooks/typhoeus.rb000066400000000000000000000116541347305653000207370ustar00rootroot00000000000000require 'vcr/util/version_checker' require 'vcr/request_handler' require 'typhoeus' if Float(Typhoeus::VERSION[/^\d+\.\d+/]) < 0.5 require 'vcr/library_hooks/typhoeus_0.4' else VCR::VersionChecker.new('Typhoeus', Typhoeus::VERSION, '0.5.0').check_version! module VCR class LibraryHooks # @private module Typhoeus # @private class RequestHandler < ::VCR::RequestHandler attr_reader :request def initialize(request) @request = request request.block_connection = false if VCR.turned_on? end def vcr_request @vcr_request ||= VCR::Request.new \ request.options.fetch(:method, :get), request.url, request_body, request.options.fetch(:headers, {}) end private def externally_stubbed? ::Typhoeus::Expectation.find_by(request) end def set_typed_request_for_after_hook(*args) super request.instance_variable_set(:@__typed_vcr_request, @after_hook_typed_request) end def on_unhandled_request invoke_after_request_hook(nil) super end def on_stubbed_by_vcr_request response = ::Typhoeus::Response.new \ :http_version => stubbed_response.http_version, :code => stubbed_response.status.code, :status_message => stubbed_response.status.message, :headers => stubbed_response_headers, :body => stubbed_response.body, :effective_url => stubbed_response.adapter_metadata.fetch('effective_url', request.url), :mock => true first_header_line = "HTTP/#{stubbed_response.http_version} #{response.code} #{response.status_message}\r\n" response.instance_variable_set(:@first_header_line, first_header_line) response.instance_variable_get(:@options)[:response_headers] = first_header_line + response.headers.map { |k,v| "#{k}: #{v}"}.join("\r\n") response end def stubbed_response_headers @stubbed_response_headers ||= {}.tap do |hash| stubbed_response.headers.each do |key, values| hash[key] = values.size == 1 ? values.first : values end if stubbed_response.headers end end if ::Typhoeus::Request.method_defined?(:encoded_body) def request_body request.encoded_body end else def request_body request.options.fetch(:body, "") end end end # @private class << self def vcr_response_from(response) VCR::Response.new \ VCR::ResponseStatus.new(response.code, response.status_message), response.headers, response.body, response.http_version, { "effective_url" => response.effective_url } end def collect_chunks(request) chunks = '' request.on_body.unshift( Proc.new do |body, response| chunks += body request.instance_variable_set(:@chunked_body, chunks) end ) end def restore_body_from_chunks(response, request) response.options[:response_body] = request.instance_variable_get(:@chunked_body) end end ::Typhoeus.on_complete do |response| request = response.request restore_body_from_chunks(response, request) if request.streaming? unless VCR.library_hooks.disabled?(:typhoeus) vcr_response = vcr_response_from(response) typed_vcr_request = request.send(:remove_instance_variable, :@__typed_vcr_request) unless request.response.mock http_interaction = VCR::HTTPInteraction.new(typed_vcr_request, vcr_response) VCR.record_http_interaction(http_interaction) end VCR.configuration.invoke_hook(:after_http_request, typed_vcr_request, vcr_response) end end ::Typhoeus.before do |request| collect_chunks(request) if request.streaming? if response = VCR::LibraryHooks::Typhoeus::RequestHandler.new(request).handle request.on_headers.each { |cb| cb.call(response) } request.on_body.each { |cb| cb.call(response.body, response) } request.finish(response) else true end end end end end end VCR.configuration.after_library_hooks_loaded do # ensure WebMock's Typhoeus adapter does not conflict with us here # (i.e. to double record requests or whatever). if defined?(WebMock::HttpLibAdapters::TyphoeusAdapter) WebMock::HttpLibAdapters::TyphoeusAdapter.disable! end end vcr-5.0.0/lib/vcr/library_hooks/typhoeus_0.4.rb000066400000000000000000000062561347305653000213220ustar00rootroot00000000000000VCR::VersionChecker.new('Typhoeus', Typhoeus::VERSION, '0.3.2').check_version! module VCR class LibraryHooks # @private module Typhoeus # @private class RequestHandler < ::VCR::RequestHandler attr_reader :request def initialize(request) @request = request end def vcr_request @vcr_request ||= VCR::Request.new \ request.method, request.url, request.body, request.headers end private def externally_stubbed? ::Typhoeus::Hydra.stubs.detect { |stub| stub.matches?(request) } end def set_typed_request_for_after_hook(*args) super request.instance_variable_set(:@__typed_vcr_request, @after_hook_typed_request) end def on_unhandled_request invoke_after_request_hook(nil) super end def on_stubbed_by_vcr_request ::Typhoeus::Response.new \ :http_version => stubbed_response.http_version, :code => stubbed_response.status.code, :status_message => stubbed_response.status.message, :headers_hash => stubbed_response_headers, :body => stubbed_response.body end def stubbed_response_headers @stubbed_response_headers ||= {}.tap do |hash| stubbed_response.headers.each do |key, values| hash[key] = values.size == 1 ? values.first : values end if stubbed_response.headers end end end # @private def self.vcr_response_from(response) VCR::Response.new \ VCR::ResponseStatus.new(response.code, response.status_message), response.headers_hash, response.body, response.http_version end ::Typhoeus::Hydra.after_request_before_on_complete do |request| unless VCR.library_hooks.disabled?(:typhoeus) vcr_response = vcr_response_from(request.response) typed_vcr_request = request.send(:remove_instance_variable, :@__typed_vcr_request) unless request.response.mock? http_interaction = VCR::HTTPInteraction.new(typed_vcr_request, vcr_response) VCR.record_http_interaction(http_interaction) end VCR.configuration.invoke_hook(:after_http_request, typed_vcr_request, vcr_response) end end ::Typhoeus::Hydra.register_stub_finder do |request| VCR::LibraryHooks::Typhoeus::RequestHandler.new(request).handle end end end end # @private module Typhoeus class << Hydra # ensure HTTP requests are always allowed; VCR takes care of disallowing # them at the appropriate times in its hook def allow_net_connect_with_vcr?(*args) VCR.turned_on? ? true : allow_net_connect_without_vcr? end alias allow_net_connect_without_vcr? allow_net_connect? alias allow_net_connect? allow_net_connect_with_vcr? end unless Hydra.respond_to?(:allow_net_connect_with_vcr?) end VCR.configuration.after_library_hooks_loaded do ::Kernel.warn "WARNING: VCR's Typhoeus 0.4 integration is deprecated and will be removed in VCR 3.0." end vcr-5.0.0/lib/vcr/library_hooks/webmock.rb000066400000000000000000000124431347305653000205030ustar00rootroot00000000000000require 'vcr/util/version_checker' require 'vcr/request_handler' require 'webmock' VCR::VersionChecker.new('WebMock', WebMock.version, '1.8.0').check_version! WebMock.enable! module VCR class LibraryHooks # @private module WebMock extend self @global_hook_disabled_requests = {} def with_global_hook_disabled(request) global_hook_disabled_requests << request begin yield ensure global_hook_disabled_requests.delete(request) end end def global_hook_disabled?(request) requests = @global_hook_disabled_requests[Thread.current.object_id] requests && requests.include?(request) end def global_hook_disabled_requests requests = @global_hook_disabled_requests[Thread.current.object_id] return requests if requests ObjectSpace.define_finalizer(Thread.current, lambda { @global_hook_disabled_requests.delete(Thread.current.object_id) }) @global_hook_disabled_requests[Thread.current.object_id] = [] end # @private module Helpers def vcr_request_for(webmock_request) VCR::Request.new \ webmock_request.method, webmock_request.uri.to_s, webmock_request.body, request_headers_for(webmock_request) end # @private def vcr_response_for(webmock_response) VCR::Response.new \ VCR::ResponseStatus.new(*webmock_response.status), webmock_response.headers, webmock_response.body, nil end if defined?(::Excon) # @private def request_headers_for(webmock_request) return nil unless webmock_request.headers # WebMock hooks deeply into a Excon at a place where it manually adds a "Host" # header, but this isn't a header we actually care to store... webmock_request.headers.dup.tap do |headers| headers.delete("Host") end end else # @private def request_headers_for(webmock_request) webmock_request.headers end end def typed_request_for(webmock_request, remove = false) if webmock_request.instance_variables.find { |v| v.to_sym == :@__typed_vcr_request } meth = remove ? :remove_instance_variable : :instance_variable_get return webmock_request.send(meth, :@__typed_vcr_request) end warn <<-EOS.gsub(/^\s+\|/, '') |WARNING: There appears to be a bug in WebMock's after_request hook | and VCR is attempting to work around it. Some VCR features | may not work properly. EOS Request::Typed.new(vcr_request_for(webmock_request), :unknown) end end class RequestHandler < ::VCR::RequestHandler include Helpers attr_reader :request def initialize(request) @request = request end private def externally_stubbed? # prevent infinite recursion... VCR::LibraryHooks::WebMock.with_global_hook_disabled(request) do ::WebMock.registered_request?(request) end end def set_typed_request_for_after_hook(*args) super request.instance_variable_set(:@__typed_vcr_request, @after_hook_typed_request) end def vcr_request @vcr_request ||= vcr_request_for(request) end def on_externally_stubbed_request # nil allows WebMock to handle the request nil end def on_unhandled_request invoke_after_request_hook(nil) super end def on_stubbed_by_vcr_request { :body => stubbed_response.body.dup, # Excon mutates the body, so we must dup it :-( :status => [stubbed_response.status.code.to_i, stubbed_response.status.message], :headers => stubbed_response.headers } end end extend Helpers ::WebMock.globally_stub_request do |req| global_hook_disabled?(req) ? nil : RequestHandler.new(req).handle end ::WebMock.after_request(:real_requests_only => true) do |request, response| unless VCR.library_hooks.disabled?(:webmock) http_interaction = VCR::HTTPInteraction.new \ typed_request_for(request), vcr_response_for(response) VCR.record_http_interaction(http_interaction) end end ::WebMock.after_request do |request, response| unless VCR.library_hooks.disabled?(:webmock) VCR.configuration.invoke_hook \ :after_http_request, typed_request_for(request, :remove), vcr_response_for(response) end end end end end # @private module WebMock class << self # ensure HTTP requests are always allowed; VCR takes care of disallowing # them at the appropriate times in its hook def net_connect_allowed_with_vcr?(*args) VCR.turned_on? ? true : net_connect_allowed_without_vcr?(*args) end alias net_connect_allowed_without_vcr? net_connect_allowed? alias net_connect_allowed? net_connect_allowed_with_vcr? end unless respond_to?(:net_connect_allowed_with_vcr?) end vcr-5.0.0/lib/vcr/linked_cassette.rb000066400000000000000000000036411347305653000173460ustar00rootroot00000000000000require 'delegate' require 'vcr/errors' module VCR # A Cassette wrapper for linking cassettes from another thread class LinkedCassette < SimpleDelegator # An enumerable lazily wrapping a list of cassettes that a context is using class CassetteList include Enumerable # Creates a new list of context-owned cassettes and linked cassettes # @param [Array] context-owned cassettes # @param [Array] context-unowned (linked) cassettes def initialize(cassettes, linked_cassettes) @cassettes = cassettes @linked_cassettes = linked_cassettes end # Yields linked cassettes first, and then context-owned cassettes def each @linked_cassettes.each do |cassette| yield wrap(cassette) end @cassettes.each do |cassette| yield cassette end end # Provide last implementation, which is not provided by Enumerable def last cassette = @cassettes.last return cassette if cassette cassette = @linked_cassettes.last wrap(cassette) if cassette end # Provide size implementation, which is not provided by Enumerable def size @cassettes.size + @linked_cassettes.size end protected def wrap(cassette) if cassette.linked? cassette else LinkedCassette.new(cassette) end end end # Create a new CassetteList # @param [Array] context-owned cassettes # @param [Array] context-unowned (linked) cassettes def self.list(cassettes, linked_cassettes) CassetteList.new(cassettes, linked_cassettes) end # Prevents cassette ejection by raising EjectLinkedCassetteError def eject(*args) raise Errors::EjectLinkedCassetteError, "cannot eject a cassette inserted by a parent thread" end # @return [Boolean] true def linked? true end end end vcr-5.0.0/lib/vcr/middleware/000077500000000000000000000000001347305653000157715ustar00rootroot00000000000000vcr-5.0.0/lib/vcr/middleware/excon.rb000066400000000000000000000144601347305653000174370ustar00rootroot00000000000000require 'excon' require 'vcr/request_handler' require 'vcr/util/version_checker' VCR::VersionChecker.new('Excon', Excon::VERSION, '0.25.2').check_version! module VCR # Contains middlewares for use with different libraries. module Middleware # Contains Excon middlewares. module Excon # One part of the Excon middleware that uses VCR to record # and replay HTTP requests made through Excon. # # @private class Request < ::Excon::Middleware::Base # @private def request_call(params) params[:vcr_request_handler] = request_handler = RequestHandler.new request_handler.before_request(params) super end end # One part of the Excon middleware that uses VCR to record # and replay HTTP requests made through Excon. # # @private class Response < ::Excon::Middleware::Base # @private def response_call(params) complete_request(params) super end def error_call(params) complete_request(params) super end private def complete_request(params) if handler = params.delete(:vcr_request_handler) handler.after_request(params[:response]) end end end # Handles a single Excon request. # # @private class RequestHandler < ::VCR::RequestHandler def initialize @request_params = nil @response_body_reader = nil @should_record = false end # Performs before_request processing based on the provided # request_params. # # @private def before_request(request_params) @request_params = request_params @response_body_reader = create_response_body_reader handle end # Performs after_request processing based on the provided response. # # @private def after_request(response) vcr_response = vcr_response_for(response) if should_record? VCR.record_http_interaction(VCR::HTTPInteraction.new(vcr_request, vcr_response)) end invoke_after_request_hook(vcr_response) end def ensure_response_body_can_be_read_for_error_case # Excon does not invoke the `:response_block` when an error # has occurred, so we need to be sure to use the non-streaming # body reader. @response_body_reader = NonStreamingResponseBodyReader end attr_reader :request_params, :response_body_reader private def externally_stubbed? !!::Excon.stub_for(request_params) end def should_record? @should_record end def on_stubbed_by_vcr_request request_params[:response] = { :body => stubbed_response.body.dup, # Excon mutates the body, so we must dup it :-( :headers => normalized_headers(stubbed_response.headers || {}), :status => stubbed_response.status.code } end def on_recordable_request @should_record = true end def create_response_body_reader block = request_params[:response_block] return NonStreamingResponseBodyReader unless block StreamingResponseBodyReader.new(block).tap do |response_block_wrapper| request_params[:response_block] = response_block_wrapper end end def vcr_request @vcr_request ||= begin headers = request_params[:headers].dup headers.delete("Host") VCR::Request.new \ request_params[:method], uri, request_params[:body], headers end end def vcr_response_for(response) return nil if response.nil? VCR::Response.new( VCR::ResponseStatus.new(response.fetch(:status), nil), response.fetch(:headers), response_body_reader.read_body_from(response), nil ) end def normalized_headers(headers) normalized = {} headers.each do |k, v| v = v.join(', ') if v.respond_to?(:join) normalized[k] = v end normalized end if defined?(::Excon::Utils) && ::Excon::Utils.respond_to?(:request_uri) def uri @uri ||= "#{::Excon::Utils.request_uri(request_params)}" end else require 'vcr/middleware/excon/legacy_methods' include LegacyMethods def uri @uri ||= "#{request_params[:scheme]}://#{request_params[:host]}:#{request_params[:port]}#{request_params[:path]}#{query}" end end end # Wraps an Excon streaming `:response_block`, so that we can # accumulate the response as it streams back from the real HTTP # server in order to record it. # # @private class StreamingResponseBodyReader def initialize(response_block) @response_block = response_block @chunks = [] end # @private def call(chunk, remaining_bytes, total_bytes) @chunks << chunk @response_block.call(chunk, remaining_bytes, total_bytes) end # Provides a duck-typed interface that matches that of # `NonStreamingResponseBodyReader`. The request handler # will use this to get the response body. # # @private def read_body_from(response_params) if @chunks.none? # Not sure why, but sometimes the body comes through the params # instead of via the streaming block even when the block was # configured. response_params[:body] else @chunks.join('') end end end # Reads the body when no streaming is done. # # @private class NonStreamingResponseBodyReader # Provides a duck-typed interface that matches that of # `StreamingResponseBodyReader`. The request handler # will use this to get the response body. # # @private def self.read_body_from(response_params) response_params.fetch(:body) end end end end end vcr-5.0.0/lib/vcr/middleware/excon/000077500000000000000000000000001347305653000171055ustar00rootroot00000000000000vcr-5.0.0/lib/vcr/middleware/excon/legacy_methods.rb000066400000000000000000000016561347305653000224310ustar00rootroot00000000000000module VCR module Middleware module Excon # Contains legacy methods only needed when integrating with older versions # of Excon. # @api private module LegacyMethods # based on: # https://github.com/geemus/excon/blob/v0.7.8/lib/excon/connection.rb#L117-132 def query @query ||= case request_params[:query] when String "?#{request_params[:query]}" when Hash qry = '?' for key, values in request_params[:query] if values.nil? qry << key.to_s << '&' else for value in [*values] qry << key.to_s << '=' << CGI.escape(value.to_s) << '&' end end end qry.chop! # remove trailing '&' else '' end end end end end end vcr-5.0.0/lib/vcr/middleware/faraday.rb000066400000000000000000000073311347305653000177310ustar00rootroot00000000000000require 'faraday' require 'vcr/util/version_checker' require 'vcr/request_handler' VCR::VersionChecker.new('Faraday', Faraday::VERSION, '0.7.0').check_version! module VCR # Contains middlewares for use with different libraries. module Middleware # Faraday middleware that VCR uses to record and replay HTTP requests made through # Faraday. # # @note You can either insert this middleware into the Faraday middleware stack # yourself or configure {VCR::Configuration#hook_into} to hook into `:faraday`. class Faraday include VCR::Deprecations::Middleware::Faraday # Constructs a new instance of the Faraday middleware. # # @param [#call] app the faraday app def initialize(app) super @app = app end # Handles the HTTP request being made through Faraday # # @param [Hash] env the Faraday request env hash def call(env) return @app.call(env) if VCR.library_hooks.disabled?(:faraday) RequestHandler.new(@app, env).handle end # @private class RequestHandler < ::VCR::RequestHandler attr_reader :app, :env def initialize(app, env) @app, @env = app, env @has_on_complete_hook = false end def handle # Faraday must be exlusive here in case another library hook is being used. # We don't want double recording/double playback. VCR.library_hooks.exclusive_hook = :faraday super ensure response = defined?(@vcr_response) ? @vcr_response : nil invoke_after_request_hook(response) unless delay_finishing? end private def delay_finishing? !!env[:parallel_manager] && @has_on_complete_hook end def vcr_request @vcr_request ||= VCR::Request.new \ env[:method], env[:url].to_s, raw_body_from(env[:body]), env[:request_headers] end def raw_body_from(body) return body unless body.respond_to?(:read) body.read.tap do |b| body.rewind if body.respond_to?(:rewind) end end def response_for(response) # reason_phrase is a new addition to Faraday::Response, # so maintain backward compatibility reason = response.respond_to?(:reason_phrase) ? response.reason_phrase : nil VCR::Response.new( VCR::ResponseStatus.new(response.status, reason), response.headers, raw_body_from(response.body), nil ) end def on_ignored_request response = app.call(env) @vcr_response = response_for(response) response end def on_stubbed_by_vcr_request headers = env[:response_headers] ||= ::Faraday::Utils::Headers.new headers.update stubbed_response.headers if stubbed_response.headers env.update :status => stubbed_response.status.code, :body => stubbed_response.body @vcr_response = stubbed_response faraday_response = ::Faraday::Response.new faraday_response.finish(env) env[:response] = faraday_response end def on_recordable_request @has_on_complete_hook = true response = app.call(env) response.on_complete do @vcr_response = response_for(response) VCR.record_http_interaction(VCR::HTTPInteraction.new(vcr_request, @vcr_response)) invoke_after_request_hook(@vcr_response) if delay_finishing? end end def invoke_after_request_hook(response) super VCR.library_hooks.exclusive_hook = nil end end end end end vcr-5.0.0/lib/vcr/middleware/rack.rb000066400000000000000000000045221347305653000172410ustar00rootroot00000000000000module VCR module Middleware # Object yielded by VCR's {Rack} middleware that allows you to configure # the cassette dynamically based on the rack env. class CassetteArguments # @private def initialize @name = nil @options = {} end # Sets (and gets) the cassette name. # # @param [#to_s] name the cassette name # @return [#to_s] the cassette name def name(name = nil) @name = name if name @name end # Sets (and gets) the cassette options. # # @param [Hash] options the cassette options # @return [Hash] the cassette options def options(options = {}) @options.merge!(options) end end # Rack middleware that uses a VCR cassette for each incoming HTTP request. # # @example # app = Rack::Builder.new do # use VCR::Middleware::Rack do |cassette, env| # cassette.name "rack/#{env['SERVER_NAME']}" # cassette.options :record => :new_episodes # end # # run MyRackApp # end # # @note This will record/replay _outbound_ HTTP requests made by your rack app. class Rack include VCR::VariableArgsBlockCaller # Constructs a new instance of VCR's rack middleware. # # @param [#call] app the rack app # @yield the cassette configuration block # @yieldparam [CassetteArguments] cassette the cassette configuration object # @yieldparam [(optional) Hash] env the rack env hash # @raise [ArgumentError] if no configuration block is provided def initialize(app, &block) raise ArgumentError.new("You must provide a block to set the cassette options") unless block @app, @cassette_arguments_block, @mutex = app, block, Mutex.new end # Implements the rack middleware interface. # # @param [Hash] env the rack env hash # @return [Array(Integer, Hash, #each)] the rack response def call(env) @mutex.synchronize do VCR.use_cassette(*cassette_arguments(env)) do @app.call(env) end end end private def cassette_arguments(env) arguments = CassetteArguments.new call_block(@cassette_arguments_block, arguments, env) [arguments.name, arguments.options] end end end end vcr-5.0.0/lib/vcr/request_handler.rb000066400000000000000000000060601347305653000173700ustar00rootroot00000000000000module VCR # @private class RequestHandler include Logger::Mixin def handle log "Handling request: #{request_summary} (disabled: #{disabled?})" invoke_before_request_hook req_type = request_type(:consume_stub) log "Identified request type (#{req_type}) for #{request_summary}" # The before_request hook can change the type of request # (i.e. by inserting a cassette), so we need to query the # request type again. # # Likewise, the main handler logic can modify what # #request_type would return (i.e. when a response stub is # used), so we need to store the request type for the # after_request hook. set_typed_request_for_after_hook(req_type) send "on_#{req_type}_request" end private def set_typed_request_for_after_hook(request_type) @after_hook_typed_request = Request::Typed.new(vcr_request, request_type) end def request_type(consume_stub = false) case when externally_stubbed? then :externally_stubbed when should_ignore? then :ignored when has_response_stub?(consume_stub) then :stubbed_by_vcr when VCR.real_http_connections_allowed? then :recordable else :unhandled end end def invoke_before_request_hook return if disabled? || !VCR.configuration.has_hooks_for?(:before_http_request) typed_request = Request::Typed.new(vcr_request, request_type) VCR.configuration.invoke_hook(:before_http_request, typed_request) end def invoke_after_request_hook(vcr_response) return if disabled? VCR.configuration.invoke_hook(:after_http_request, @after_hook_typed_request, vcr_response) end def externally_stubbed? false end def should_ignore? disabled? || VCR.request_ignorer.ignore?(vcr_request) end def disabled? VCR.library_hooks.disabled?(library_name) end def has_response_stub?(consume_stub) if consume_stub stubbed_response else VCR.http_interactions.has_interaction_matching?(vcr_request) end end def stubbed_response @stubbed_response ||= VCR.http_interactions.response_for(vcr_request) end def library_name # extracts `:typhoeus` from `VCR::LibraryHooks::Typhoeus::RequestHandler` @library_name ||= self.class.name.split('::')[-2].downcase.to_sym end # Subclasses can implement these def on_externally_stubbed_request end def on_ignored_request end def on_stubbed_by_vcr_request end def on_recordable_request end def on_unhandled_request raise VCR::Errors::UnhandledHTTPRequestError.new(vcr_request) end def request_summary request_matchers = if cass = VCR.current_cassette cass.match_requests_on else VCR.configuration.default_cassette_options[:match_requests_on] end super(vcr_request, request_matchers) end def log_prefix "[#{library_name}] " end end end vcr-5.0.0/lib/vcr/request_ignorer.rb000066400000000000000000000015241347305653000174200ustar00rootroot00000000000000require 'set' require 'vcr/util/hooks' module VCR # @private class RequestIgnorer include VCR::Hooks define_hook :ignore_request LOCALHOST_ALIASES = %w( localhost 127.0.0.1 0.0.0.0 ) def initialize ignore_request do |request| host = request.parsed_uri.host ignored_hosts.include?(host) end end def ignore_localhost=(value) if value ignore_hosts(*LOCALHOST_ALIASES) else ignored_hosts.reject! { |h| LOCALHOST_ALIASES.include?(h) } end end def ignore_hosts(*hosts) ignored_hosts.merge(hosts) end def unignore_hosts(*hosts) ignored_hosts.subtract(hosts) end def ignore?(request) invoke_hook(:ignore_request, request).any? end private def ignored_hosts @ignored_hosts ||= Set.new end end end vcr-5.0.0/lib/vcr/request_matcher_registry.rb000066400000000000000000000104161347305653000213260ustar00rootroot00000000000000require 'vcr/errors' module VCR # Keeps track of the different request matchers. class RequestMatcherRegistry # The default request matchers used for any cassette that does not # specify request matchers. DEFAULT_MATCHERS = [:method, :uri] # @private class Matcher < Struct.new(:callable) def matches?(request_1, request_2) callable.call(request_1, request_2) end end # @private class URIWithoutParamsMatcher < Struct.new(:params_to_ignore) def partial_uri_from(request) request.parsed_uri.tap do |uri| return uri unless uri.query # ignore uris without params, e.g. "http://example.com/" uri.query = uri.query.split('&').tap { |params| params.map! do |p| key, value = p.split('=') key.gsub!(/\[\]\z/, '') # handle params like tag[]= [key, value] end params.reject! { |p| params_to_ignore.include?(p.first) } params.map! { |p| p.join('=') } }.join('&') uri.query = nil if uri.query.empty? end end def call(request_1, request_2) partial_uri_from(request_1) == partial_uri_from(request_2) end def to_proc lambda { |r1, r2| call(r1, r2) } end end # @private def initialize @registry = {} register_built_ins end # @private def register(name, &block) if @registry.has_key?(name) warn "WARNING: There is already a VCR request matcher registered for #{name.inspect}. Overriding it." end @registry[name] = Matcher.new(block) end # @private def [](matcher) @registry.fetch(matcher) do matcher.respond_to?(:call) ? Matcher.new(matcher) : raise_unregistered_matcher_error(matcher) end end # Builds a dynamic request matcher that matches on a URI while ignoring the # named query parameters. This is useful for dealing with non-deterministic # URIs (i.e. that have a timestamp or request signature parameter). # # @example # without_timestamp = VCR.request_matchers.uri_without_param(:timestamp) # # # use it directly... # VCR.use_cassette('example', :match_requests_on => [:method, without_timestamp]) { } # # # ...or register it as a named matcher # VCR.configure do |c| # c.register_request_matcher(:uri_without_timestamp, &without_timestamp) # end # # VCR.use_cassette('example', :match_requests_on => [:method, :uri_without_timestamp]) { } # # @param ignores [Array<#to_s>] The names of the query parameters to ignore # @return [#call] the request matcher def uri_without_params(*ignores) uri_without_param_matchers[ignores] end alias uri_without_param uri_without_params private def uri_without_param_matchers @uri_without_param_matchers ||= Hash.new do |hash, params| params = params.map(&:to_s) hash[params] = URIWithoutParamsMatcher.new(params) end end def raise_unregistered_matcher_error(name) raise Errors::UnregisteredMatcherError.new \ "There is no matcher registered for #{name.inspect}. " + "Did you mean one of #{@registry.keys.map(&:inspect).join(', ')}?" end def register_built_ins register(:method) { |r1, r2| r1.method == r2.method } register(:uri) { |r1, r2| r1.uri == r2.uri } register(:body) { |r1, r2| r1.body == r2.body } register(:headers) { |r1, r2| r1.headers == r2.headers } register(:host) do |r1, r2| r1.parsed_uri.host == r2.parsed_uri.host end register(:path) do |r1, r2| r1.parsed_uri.path == r2.parsed_uri.path end register(:query) do |r1, r2| VCR.configuration.query_parser.call(r1.parsed_uri.query.to_s) == VCR.configuration.query_parser.call(r2.parsed_uri.query.to_s) end try_to_register_body_as_json end def try_to_register_body_as_json begin require 'json' rescue LoadError return end register(:body_as_json) do |r1, r2| begin r1.body == r2.body || JSON.parse(r1.body) == JSON.parse(r2.body) rescue JSON::ParserError false end end end end end vcr-5.0.0/lib/vcr/structs.rb000066400000000000000000000433111347305653000157120ustar00rootroot00000000000000require 'base64' require 'delegate' require 'time' module VCR # @private module Normalizers # @private module Body def self.included(klass) klass.extend ClassMethods end # @private module ClassMethods def body_from(hash_or_string) return hash_or_string unless hash_or_string.is_a?(Hash) hash = hash_or_string if hash.has_key?('base64_string') string = Base64.decode64(hash['base64_string']) force_encode_string(string, hash['encoding']) else try_encode_string(hash['string'], hash['encoding']) end end if "".respond_to?(:encoding) def force_encode_string(string, encoding) return string unless encoding string.force_encoding(encoding) end def try_encode_string(string, encoding) return string if encoding.nil? || string.encoding.name == encoding # ASCII-8BIT just means binary, so encoding to it is nonsensical # and yet "\u00f6".encode("ASCII-8BIT") raises an error. # Instead, we'll force encode it (essentially just tagging it as binary) return string.force_encoding(encoding) if encoding == "ASCII-8BIT" string.encode(encoding) rescue EncodingError => e struct_type = name.split('::').last.downcase warn "VCR: got `#{e.class.name}: #{e.message}` while trying to encode the #{string.encoding.name} " + "#{struct_type} body to the original body encoding (#{encoding}). Consider using the " + "`:preserve_exact_body_bytes` option to work around this." return string end else def force_encode_string(string, encoding) string end def try_encode_string(string, encoding) string end end end def initialize(*args) super if body && !body.is_a?(String) raise ArgumentError, "#{self.class} initialized with an invalid body: #{body.inspect}." end # Ensure that the body is a raw string, in case the string instance # has been subclassed or extended with additional instance variables # or attributes, so that it is serialized to YAML as a raw string. # This is needed for rest-client. See this ticket for more info: # http://github.com/myronmarston/vcr/issues/4 self.body = String.new(body.to_s) end private def serializable_body # Ensure it's just a string, and not a string with some # extra state, as such strings serialize to YAML with # all the extra state. body = String.new(self.body.to_s) if VCR.configuration.preserve_exact_body_bytes_for?(self) base_body_hash(body).merge('base64_string' => Base64.encode64(body)) else base_body_hash(body).merge('string' => body) end end if ''.respond_to?(:encoding) def base_body_hash(body) { 'encoding' => body.encoding.name } end else def base_body_hash(body) { } end end end # @private module Header def initialize(*args) super normalize_headers end private def normalize_headers new_headers = {} @normalized_header_keys = Hash.new {|h,k| k } headers.each do |k, v| val_array = case v when Array then v when nil then [] else [v] end new_headers[String.new(k)] = convert_to_raw_strings(val_array) @normalized_header_keys[k.downcase] = k end if headers self.headers = new_headers end def header_key(key) key = @normalized_header_keys[key.downcase] key if headers.has_key? key end def get_header(key) key = header_key(key) headers[key] if key end def edit_header(key, value = nil) if key = header_key(key) value ||= yield headers[key] headers[key] = Array(value) end end def delete_header(key) if key = header_key(key) @normalized_header_keys.delete key.downcase headers.delete key end end def convert_to_raw_strings(array) # Ensure the values are raw strings. # Apparently for Paperclip uploads to S3, headers # get serialized with some extra stuff which leads # to a seg fault. See this issue for more info: # https://github.com/myronmarston/vcr/issues#issue/39 array.map do |v| case v when String; String.new(v) when Array; convert_to_raw_strings(v) else v end end end end end # The request of an {HTTPInteraction}. # # @attr [Symbol] method the HTTP method (i.e. :head, :options, :get, :post, :put, :patch or :delete) # @attr [String] uri the request URI # @attr [String, nil] body the request body # @attr [Hash{String => Array}] headers the request headers class Request < Struct.new(:method, :uri, :body, :headers) include Normalizers::Header include Normalizers::Body def initialize(*args) skip_port_stripping = false if args.last == :skip_port_stripping skip_port_stripping = true args.pop end super(*args) self.method = self.method.to_s.downcase.to_sym if self.method self.uri = without_standard_port(self.uri) unless skip_port_stripping end # Builds a serializable hash from the request data. # # @return [Hash] hash that represents this request and can be easily # serialized. # @see Request.from_hash def to_hash { 'method' => method.to_s, 'uri' => uri, 'body' => serializable_body, 'headers' => headers } end # Constructs a new instance from a hash. # # @param [Hash] hash the hash to use to construct the instance. # @return [Request] the request def self.from_hash(hash) method = hash['method'] method &&= method.to_sym new method, hash['uri'], body_from(hash['body']), hash['headers'], :skip_port_stripping end # Parses the URI using the configured `uri_parser`. # # @return [#schema, #host, #port, #path, #query] A parsed URI object. def parsed_uri VCR.configuration.uri_parser.parse(uri) end @@object_method = Object.instance_method(:method) def method(*args) return super if args.empty? @@object_method.bind(self).call(*args) end # Decorates a {Request} with its current type. class Typed < DelegateClass(self) # @return [Symbol] One of `:ignored`, `:stubbed`, `:recordable` or `:unhandled`. attr_reader :type # @param [Request] request the request # @param [Symbol] type the type. Should be one of `:ignored`, `:stubbed`, `:recordable` or `:unhandled`. def initialize(request, type) @type = type super(request) end # @return [Boolean] whether or not this request is being ignored def ignored? type == :ignored end # @return [Boolean] whether or not this request is being stubbed by VCR # @see #externally_stubbed? # @see #stubbed? def stubbed_by_vcr? type == :stubbed_by_vcr end # @return [Boolean] whether or not this request is being stubbed by an # external library (such as WebMock). # @see #stubbed_by_vcr? # @see #stubbed? def externally_stubbed? type == :externally_stubbed end # @return [Boolean] whether or not this request will be recorded. def recordable? type == :recordable end # @return [Boolean] whether or not VCR knows how to handle this request. def unhandled? type == :unhandled end # @return [Boolean] whether or not this request will be made for real. # @note VCR allows `:ignored` and `:recordable` requests to be made for real. def real? ignored? || recordable? end # @return [Boolean] whether or not this request will be stubbed. # It may be stubbed by an external library or by VCR. # @see #stubbed_by_vcr? # @see #externally_stubbed? def stubbed? stubbed_by_vcr? || externally_stubbed? end undef method end # Provides fiber-awareness for the {VCR::Configuration#around_http_request} hook. class FiberAware < DelegateClass(Typed) # Yields the fiber so the request can proceed. # # @return [VCR::Response] the response from the request def proceed Fiber.yield end # Builds a proc that allows the request to proceed when called. # This allows you to treat the request as a proc and pass it on # to a method that yields (at which point the request will proceed). # # @return [Proc] the proc def to_proc lambda { proceed } end undef method end private def without_standard_port(uri) return uri if uri.nil? u = parsed_uri return uri unless [['http', 80], ['https', 443]].include?([u.scheme, u.port]) u.port = nil u.to_s end end # The response of an {HTTPInteraction}. # # @attr [ResponseStatus] status the status of the response # @attr [Hash{String => Array}] headers the response headers # @attr [String] body the response body # @attr [nil, String] http_version the HTTP version # @attr [Hash] adapter_metadata Additional metadata used by a specific VCR adapter. class Response < Struct.new(:status, :headers, :body, :http_version, :adapter_metadata) include Normalizers::Header include Normalizers::Body def initialize(*args) super(*args) self.adapter_metadata ||= {} end # Builds a serializable hash from the response data. # # @return [Hash] hash that represents this response # and can be easily serialized. # @see Response.from_hash def to_hash { 'status' => status.to_hash, 'headers' => headers, 'body' => serializable_body, 'http_version' => http_version }.tap do |hash| hash['adapter_metadata'] = adapter_metadata unless adapter_metadata.empty? end end # Constructs a new instance from a hash. # # @param [Hash] hash the hash to use to construct the instance. # @return [Response] the response def self.from_hash(hash) new ResponseStatus.from_hash(hash.fetch('status', {})), hash['headers'], body_from(hash['body']), hash['http_version'], hash['adapter_metadata'] end # Updates the Content-Length response header so that it is # accurate for the response body. def update_content_length_header edit_header('Content-Length') { body ? body.bytesize.to_s : '0' } end # The type of encoding. # # @return [String] encoding type def content_encoding enc = get_header('Content-Encoding') and enc.first end # Checks if the type of encoding is one of "gzip" or "deflate". def compressed? %w[ gzip deflate ].include? content_encoding end # Checks if VCR decompressed the response body def vcr_decompressed? adapter_metadata['vcr_decompressed'] end # Decodes the compressed body and deletes evidence that it was ever compressed. # # @return self # @raise [VCR::Errors::UnknownContentEncodingError] if the content encoding # is not a known encoding. def decompress self.class.decompress(body, content_encoding) { |new_body| self.body = new_body update_content_length_header adapter_metadata['vcr_decompressed'] = content_encoding delete_header('Content-Encoding') } return self end # Recompresses the decompressed body according to adapter metadata. # # @raise [VCR::Errors::UnknownContentEncodingError] if the content encoding # stored in the adapter metadata is unknown def recompress type = adapter_metadata['vcr_decompressed'] new_body = begin case type when 'gzip' body_str = '' args = [StringIO.new(body_str)] args << { :encoding => 'ASCII-8BIT' } if ''.respond_to?(:encoding) writer = Zlib::GzipWriter.new(*args) writer.write(body) writer.close body_str when 'deflate' Zlib::Deflate.inflate(body) when 'identity', NilClass nil else raise Errors::UnknownContentEncodingError, "unknown content encoding: #{type}" end end if new_body self.body = new_body update_content_length_header headers['Content-Encoding'] = type end end begin require 'zlib' require 'stringio' HAVE_ZLIB = true rescue LoadError HAVE_ZLIB = false end # Decode string compressed with gzip or deflate # # @raise [VCR::Errors::UnknownContentEncodingError] if the content encoding # is not a known encoding. def self.decompress(body, type) unless HAVE_ZLIB warn "VCR: cannot decompress response; Zlib not available" return end case type when 'gzip' args = [StringIO.new(body)] args << { :encoding => 'ASCII-8BIT' } if ''.respond_to?(:encoding) yield Zlib::GzipReader.new(*args).read when 'deflate' yield Zlib::Inflate.inflate(body) when 'identity', NilClass return else raise Errors::UnknownContentEncodingError, "unknown content encoding: #{type}" end end end # The response status of an {HTTPInteraction}. # # @attr [Integer] code the HTTP status code # @attr [String] message the HTTP status message (e.g. "OK" for a status of 200) class ResponseStatus < Struct.new(:code, :message) # Builds a serializable hash from the response status data. # # @return [Hash] hash that represents this response status # and can be easily serialized. # @see ResponseStatus.from_hash def to_hash { 'code' => code, 'message' => message } end # Constructs a new instance from a hash. # # @param [Hash] hash the hash to use to construct the instance. # @return [ResponseStatus] the response status def self.from_hash(hash) new hash['code'], hash['message'] end end # Represents a single interaction over HTTP, containing a request and a response. # # @attr [Request] request the request # @attr [Response] response the response # @attr [Time] recorded_at when this HTTP interaction was recorded class HTTPInteraction < Struct.new(:request, :response, :recorded_at) def initialize(*args) super self.recorded_at ||= Time.now end # Builds a serializable hash from the HTTP interaction data. # # @return [Hash] hash that represents this HTTP interaction # and can be easily serialized. # @see HTTPInteraction.from_hash def to_hash { 'request' => request.to_hash, 'response' => response.to_hash, 'recorded_at' => recorded_at.httpdate } end # Constructs a new instance from a hash. # # @param [Hash] hash the hash to use to construct the instance. # @return [HTTPInteraction] the HTTP interaction def self.from_hash(hash) new Request.from_hash(hash.fetch('request', {})), Response.from_hash(hash.fetch('response', {})), Time.httpdate(hash.fetch('recorded_at')) end # @return [HookAware] an instance with additional capabilities # suitable for use in `before_record` and `before_playback` hooks. def hook_aware HookAware.new(self) end # Decorates an {HTTPInteraction} with additional methods useful # for a `before_record` or `before_playback` hook. class HookAware < DelegateClass(HTTPInteraction) def initialize(http_interaction) @ignored = false super end # Flags the HTTP interaction so that VCR ignores it. This is useful in # a {VCR::Configuration#before_record} or {VCR::Configuration#before_playback} # hook so that VCR does not record or play it back. # @see #ignored? def ignore! @ignored = true end # @return [Boolean] whether or not this HTTP interaction should be ignored. # @see #ignore! def ignored? !!@ignored end # Replaces a string in any part of the HTTP interaction (headers, request body, # response body, etc) with the given replacement text. # # @param [#to_s] text the text to replace # @param [#to_s] replacement_text the text to put in its place def filter!(text, replacement_text) text, replacement_text = text.to_s, replacement_text.to_s return self if [text, replacement_text].any? { |t| t.empty? } filter_object!(self, text, replacement_text) end private def filter_object!(object, text, replacement_text) if object.respond_to?(:gsub) object.gsub!(text, replacement_text) if object.include?(text) elsif Hash === object filter_hash!(object, text, replacement_text) elsif object.respond_to?(:each) # This handles nested arrays and structs object.each { |o| filter_object!(o, text, replacement_text) } end object end def filter_hash!(hash, text, replacement_text) filter_object!(hash.values, text, replacement_text) hash.keys.each do |k| new_key = filter_object!(k.dup, text, replacement_text) hash[new_key] = hash.delete(k) unless k == new_key end end end end end vcr-5.0.0/lib/vcr/tasks/000077500000000000000000000000001347305653000150015ustar00rootroot00000000000000vcr-5.0.0/lib/vcr/tasks/vcr.rake000066400000000000000000000004751347305653000164450ustar00rootroot00000000000000namespace :vcr do desc "Migrate cassettes from the VCR 1.x format to the VCR 2.x format." task :migrate_cassettes do dir = ENV.fetch('DIR') { raise "You must pass the cassette library directory as DIR=" } require 'vcr/cassette/migrator' VCR::Cassette::Migrator.new(dir).migrate! end end vcr-5.0.0/lib/vcr/test_frameworks/000077500000000000000000000000001347305653000170735ustar00rootroot00000000000000vcr-5.0.0/lib/vcr/test_frameworks/cucumber.rb000066400000000000000000000054111347305653000212260ustar00rootroot00000000000000module VCR # Provides integration with Cucumber using tags. class CucumberTags class << self # @private def tags @tags.dup end # @private def add_tag(tag) @tags << tag end end @tags = [] # @private def initialize(main_object) @main_object = main_object end # Adds `Before` and `After` cucumber hooks for the named tags that # will cause a VCR cassette to be used for scenarios with matching tags. # # @param [Array] tag_names the cucumber scenario tags # @param [(optional) Hash] options the cassette options. Specify # `:use_scenario_name => true` to automatically name the # cassette according to the scenario name. def tags(*tag_names) original_options = tag_names.last.is_a?(::Hash) ? tag_names.pop : {} tag_names.each do |tag_name| tag_name = "@#{tag_name}" unless tag_name =~ /\A@/ # It would be nice to use an Around hook here, but # cucumber has a bug: background steps do not run # within an around hook. # https://gist.github.com/652968 @main_object.Before(tag_name) do |scenario| options = original_options.dup cassette_name = if options.delete(:use_scenario_name) if scenario.respond_to?(:outline?) && scenario.outline? ScenarioNameBuilder.new(scenario).cassette_name elsif scenario.respond_to?(:scenario_outline) [ scenario.scenario_outline.feature.name.split("\n").first, scenario.scenario_outline.name, scenario.name.split("\n").first ].join("/") else [ scenario.feature.name.split("\n").first, scenario.name.split("\n").first ].join("/") end else "cucumber_tags/#{tag_name.gsub(/\A@/, '')}" end VCR.insert_cassette(cassette_name, options) end @main_object.After(tag_name) do |scenario| VCR.eject_cassette(:skip_no_unused_interactions_assertion => scenario.failed?) end self.class.add_tag(tag_name) end end alias :tag :tags # Constructs a cassette name from a Cucumber 2 scenario outline # @private class ScenarioNameBuilder def initialize(test_case) @parts = [] test_case.describe_source_to self end def cassette_name @parts.join("/") end def feature(feature) @parts.unshift feature.name self end alias scenario_outline feature def scenario(*) self end alias examples_table scenario def examples_table_row(row) @parts.unshift "| %s |" % row.values.join(" | ") self end end end end vcr-5.0.0/lib/vcr/test_frameworks/rspec.rb000066400000000000000000000033411347305653000205350ustar00rootroot00000000000000module VCR # Integrates VCR with RSpec. module RSpec # @private module Metadata extend self def configure! ::RSpec.configure do |config| vcr_cassette_name_for = lambda do |metadata| description = if metadata[:description].empty? # we have an "it { is_expected.to be something }" block metadata[:scoped_id] else metadata[:description] end example_group = if metadata.key?(:example_group) metadata[:example_group] else metadata[:parent_example_group] end if example_group [vcr_cassette_name_for[example_group], description].join('/') else description end end when_tagged_with_vcr = { :vcr => lambda { |v| !!v } } config.before(:each, when_tagged_with_vcr) do |ex| example = ex.respond_to?(:metadata) ? ex : ex.example options = example.metadata[:vcr] options = options.is_a?(Hash) ? options.dup : {} # in case it's just :vcr => true cassette_name = options.delete(:cassette_name) || vcr_cassette_name_for[example.metadata] VCR.insert_cassette(cassette_name, options) end config.after(:each, when_tagged_with_vcr) do |ex| example = ex.respond_to?(:metadata) ? ex : ex.example VCR.eject_cassette(:skip_no_unused_interactions_assertion => !!example.exception) end end end end end end vcr-5.0.0/lib/vcr/util/000077500000000000000000000000001347305653000146315ustar00rootroot00000000000000vcr-5.0.0/lib/vcr/util/hooks.rb000066400000000000000000000026721347305653000163100ustar00rootroot00000000000000require 'vcr/util/variable_args_block_caller' module VCR # @private module Hooks include VariableArgsBlockCaller # @private FilteredHook = Struct.new(:hook, :filters) do include VariableArgsBlockCaller def conditionally_invoke(*args) filters = Array(self.filters) return if filters.any? { |f| !call_block(f.to_proc, *args) } call_block(hook, *args) end end def self.included(klass) klass.class_eval do extend ClassMethods hooks_module = Module.new const_set("DefinedHooks", hooks_module) include hooks_module end end def invoke_hook(hook_type, *args) hooks[hook_type].map do |hook| hook.conditionally_invoke(*args) end end def clear_hooks hooks.clear end def hooks @hooks ||= Hash.new do |hash, hook_type| hash[hook_type] = [] end end def has_hooks_for?(hook_type) hooks[hook_type].any? end # @private module ClassMethods def define_hook(hook_type, prepend = false) placement_method = prepend ? :unshift : :<< # Put the hook methods in a module so we can override and super to these methods. self::DefinedHooks.module_eval do define_method hook_type do |*filters, &hook| hooks[hook_type].send(placement_method, FilteredHook.new(hook, filters)) end end end end end end vcr-5.0.0/lib/vcr/util/internet_connection.rb000066400000000000000000000014321347305653000212250ustar00rootroot00000000000000module VCR # Ruby 1.8 provides Ping.pingecho, but it was removed in 1.9. # This is copied, verbatim, from Ruby 1.8.7's ping.rb. require 'timeout' require "socket" # @private module Ping def pingecho(host, timeout=5, service="echo") begin Timeout.timeout(timeout) do s = TCPSocket.new(host, service) s.close end rescue Errno::ECONNREFUSED return true rescue Timeout::Error, StandardError return false end return true end module_function :pingecho end # @private module InternetConnection extend self EXAMPLE_HOST = "example.com" def available? @available = VCR::Ping.pingecho(EXAMPLE_HOST, 1, 80) unless defined?(@available) @available end end end vcr-5.0.0/lib/vcr/util/logger.rb000066400000000000000000000034151347305653000164400ustar00rootroot00000000000000module VCR # @private # Provides log message formatting helper methods. class Logger def initialize(stream) @stream = stream end def log(message, log_prefix, indentation_level = 0) indentation = ' ' * indentation_level log_message = indentation + log_prefix + message @stream.puts log_message end def request_summary(request, request_matchers) attributes = [request.method, request.uri] attributes << request.body.to_s[0, 80].inspect if request_matchers.include?(:body) attributes << request.headers.inspect if request_matchers.include?(:headers) "[#{attributes.join(" ")}]" end def response_summary(response) "[#{response.status.code} #{response.body[0, 80].inspect}]" end # @private # A null-object version of the Logger. Used when # a `debug_logger` has not been set. # # @note We used to use a null object for the `debug_logger` itself, # but some users noticed a negative perf impact from having the # logger formatting logic still executing in that case, so we # moved the null object interface up a layer to here. module Null module_function def log(*); end def request_summary(*); end def response_summary(*); end end # @private # Provides common logger helper methods that simply delegate to # the underlying logger object. module Mixin def log(message, indentation_level = 0) VCR.configuration.logger.log(message, log_prefix, indentation_level) end def request_summary(*args) VCR.configuration.logger.request_summary(*args) end def response_summary(*args) VCR.configuration.logger.response_summary(*args) end end end end vcr-5.0.0/lib/vcr/util/variable_args_block_caller.rb000066400000000000000000000003421347305653000224520ustar00rootroot00000000000000module VCR # @private module VariableArgsBlockCaller def call_block(block, *args) if block.arity >= 0 args = args.first([args.size, block.arity].min) end block.call(*args) end end end vcr-5.0.0/lib/vcr/util/version_checker.rb000066400000000000000000000022121347305653000203240ustar00rootroot00000000000000module VCR # @private class VersionChecker def initialize(library_name, library_version, min_version) @library_name = library_name @library_version = library_version @min_version = min_version @major, @minor, @patch = parse_version(library_version) @min_major, @min_minor, @min_patch = parse_version(min_version) end def check_version! raise_too_low_error if too_low? end private def too_low? compare_version == :too_low end def raise_too_low_error raise Errors::LibraryVersionTooLowError, "You are using #{@library_name} #{@library_version}. " + "VCR requires version #{version_requirement}." end def compare_version case when @major < @min_major then :too_low when @major > @min_major then :ok when @minor < @min_minor then :too_low when @minor > @min_minor then :ok when @patch < @min_patch then :too_low end end def version_requirement ">= #{@min_version}" end def parse_version(version) version.split('.').map { |v| v.to_i } end end end vcr-5.0.0/lib/vcr/version.rb000066400000000000000000000011761347305653000156730ustar00rootroot00000000000000module VCR extend self # @return [String] the current VCR version. # @note This string also has singleton methods: # # * `major` [Integer] The major version. # * `minor` [Integer] The minor version. # * `patch` [Integer] The patch version. # * `parts` [Array] List of the version parts. def version @version ||= begin string = '5.0.0' def string.parts split('.').map { |p| p.to_i } end def string.major parts[0] end def string.minor parts[1] end def string.patch parts[2] end string end end end vcr-5.0.0/script/000077500000000000000000000000001347305653000136205ustar00rootroot00000000000000vcr-5.0.0/script/cached-bundle000077500000000000000000000025341347305653000162300ustar00rootroot00000000000000#!/usr/bin/env bash # Usage: cached-bundle install --deployment # # After running `bundle`, caches the `vendor/bundle` directory to S3. # On the next run, restores the cached directory before running `bundle`. # When `Gemfile.lock` changes, the cache gets rebuilt. # # Requirements: # - Gemfile.lock # - TRAVIS_REPO_SLUG # - TRAVIS_RUBY_VERSION # - AMAZON_S3_BUCKET # - script/s3-put # - bundle # - curl # # Author: Mislav Marohnić set -e compute_md5() { local output="$(openssl md5)" echo "${output##* }" } download() { curl --tcp-nodelay -qsfL "$1" -o "$2" } bundle_path="vendor/bundle" gemfile_hash="$(cat "${BUNDLE_GEMFILE:-Gemfile}.lock" gemfiles/*.lock | compute_md5)" cache_name="${TRAVIS_RUBY_VERSION}-${gemfile_hash}.tgz" fetch_url="http://${AMAZON_S3_BUCKET}.s3.amazonaws.com/${TRAVIS_REPO_SLUG}/${cache_name}" if download "$fetch_url" "$cache_name"; then echo "Reusing cached bundle ${cache_name}" tar xzf "$cache_name" fi bundle "$@" BUNDLE_GEMFILE=gemfiles/faraday_old.gemfile bundle "$@" --path "$PWD"/vendor/bundle BUNDLE_GEMFILE=gemfiles/typhoeus_old.gemfile bundle "$@" --path "$PWD"/vendor/bundle if [ ! -f "$cache_name" ] && [ -n "$AMAZON_SECRET_ACCESS_KEY" ]; then echo "Caching \`${bundle_path}' to S3" tar czf "$cache_name" "$bundle_path" script/s3-put "$cache_name" "${AMAZON_S3_BUCKET}:${TRAVIS_REPO_SLUG}/${cache_name}" fi vcr-5.0.0/script/ci.sh000077500000000000000000000061441347305653000145570ustar00rootroot00000000000000#!/bin/bash set -e STATUS=0 warnings="${TMPDIR:-/tmp}/vcr-warnings.$$" nano_cmd="$(type -p gdate date | head -1)" nano_format="+%s%N" [ "$(uname -s)" != "Darwin" ] || nano_format="${nano_format/%N/000000000}" travis_time_start() { travis_timer_id=$(printf %08x $(( RANDOM * RANDOM ))) travis_start_time=$($nano_cmd -u "$nano_format") printf "travis_time:start:%s\r\e[0m" $travis_timer_id } travis_time_finish() { local travis_end_time=$($nano_cmd -u "$nano_format") local duration=$(($travis_end_time-$travis_start_time)) printf "travis_time:end:%s:start=%s,finish=%s,duration=%s\r\e[0m" \ $travis_timer_id $travis_start_time $travis_end_time $duration } fold() { local name="$1" local status=0 shift 1 if [ -n "$TRAVIS" ]; then printf "travis_fold:start:%s\r\e[0m" "$name" travis_time_start fi "$@" || status=$? [ -z "$TRAVIS" ] || travis_time_finish if [ "$status" -eq 0 ]; then if [ -n "$TRAVIS" ]; then printf "travis_fold:end:%s\r\e[0m" "$name" fi else STATUS="$status" fi # We keep track of the status using `STATUS` and exit with # it below, so we don't return the status here. } run() { [[ "${BUNDLE_GEMFILE##*/}" == Gemfile.* ]] && bundle install # Save warnings on stderr to a separate file RUBYOPT="$RUBYOPT -w" "$@" 2> >(tee >(grep 'warning:' >>"$warnings") | grep -v 'warning:') } fetch_warnings() { grep -F "$PWD" "$1" | \ grep -v "${PWD}/vendor/bundle" | \ grep -v "${PWD}/spec/.\+possible reference to past scope" } check_warnings() { # Display Ruby warnings from this project's source files. Abort if any were found. num="$(fetch_warnings "$warnings" | sort | uniq -c | sort -rn | tee /dev/stderr | wc -l)" rm -f "$warnings" if [ "$num" -gt 0 ]; then echo "FAILED: this test suite doesn't tolerate Ruby syntax warnings!" >&2 exit 1 fi } trap 'exit 1' INT RUBY_ENGINE="$(ruby -e 'puts defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby"')" # idea taken from: http://blog.headius.com/2010/03/jruby-startup-time-tips.html export JRUBY_OPTS='-X-C' # disable JIT since these processes are so short lived # force jRuby to use client mode JVM or a compilation mode thats as close as possible, # idea taken from https://github.com/jruby/jruby/wiki/Improving-startup-time export JAVA_OPTS='-client -XX:+TieredCompilation -XX:TieredStopAtLevel=1' export SPEC_OPTS="--backtrace --profile" if [ "$RUBY_ENGINE" = "ruby" ]; then BUNDLE_GEMFILE=Gemfile.typhoeus-0.4 fold "typhoeus-0.4" \ run script/test spec/lib/vcr/library_hooks/typhoeus_0.4_spec.rb fi BUNDLE_GEMFILE=Gemfile.faraday-0.11 fold "faraday-0.11" \ run script/test spec/lib/vcr/middleware/faraday_spec.rb spec/lib/vcr/library_hooks/faraday_spec.rb \ features/middleware/faraday.feature BUNDLE_GEMFILE=Gemfile.cucumber-1.3 fold "cucumber-1.3" \ run script/test features/test_frameworks/cucumber.feature fold "spec" run script/test spec/ fold "features" run script/test features/ check_warnings if ! bundle exec yard stats --list-undoc | tee /dev/stdout | grep -q '100.00% documented'; then echo "Failed: documentation coverage is less than 100%" STATUS=1 fi exit $STATUS vcr-5.0.0/script/s3-put000077500000000000000000000024751347305653000147110ustar00rootroot00000000000000#!/usr/bin/env bash # Usage: s3-put [:] [] # # Uploads a file to the Amazon S3 service. # Outputs the URL for the newly uploaded file. # # Requirements: # - AMAZON_ACCESS_KEY_ID # - AMAZON_SECRET_ACCESS_KEY # - openssl # - curl # # Author: Mislav Marohnić set -e authorization() { local signature="$(string_to_sign | hmac_sha1 | base64)" echo "AWS ${AMAZON_ACCESS_KEY_ID?}:${signature}" } hmac_sha1() { openssl dgst -binary -sha1 -hmac "${AMAZON_SECRET_ACCESS_KEY?}" } base64() { openssl enc -base64 } bin_md5() { openssl dgst -binary -md5 } string_to_sign() { echo "$http_method" echo "$content_md5" echo "$content_type" echo "$date" echo "x-amz-acl:$acl" printf "/$bucket/$remote_path" } date_string() { LC_TIME=C date "+%a, %d %h %Y %T %z" } file="$1" bucket="${2%%:*}" remote_path="${2#*:}" content_type="$3" if [ -z "$remote_path" ] || [ "$remote_path" = "$bucket" ]; then remote_path="${file##*/}" fi http_method=PUT acl="public-read" content_md5="$(bin_md5 < "$file" | base64)" date="$(date_string)" url="https://$bucket.s3.amazonaws.com/$remote_path" curl -qsSf -T "$file" \ -H "Authorization: $(authorization)" \ -H "x-amz-acl: $acl" \ -H "Date: $date" \ -H "Content-MD5: $content_md5" \ -H "Content-Type: $content_type" \ "$url" echo "$url" vcr-5.0.0/script/test000077500000000000000000000013171347305653000145270ustar00rootroot00000000000000#!/bin/bash # Usage: script/test [FILES...] set -e filter() { local prefix="$1" shift 1 ls -d "$@" | sed "s:$PWD/::" | grep "$prefix" } color() { if [ -t 1 ]; then printf "\e[%sm%s\e[m" "$1" "$2" else echo -n "$2" fi } run() { color "1;34" "> $*"; echo bundle exec "$@" } if [ $# -eq 0 ]; then specs=( spec/ ) cukes=( features/ ) else specs=( $(filter ^spec "$@" || true) ) cukes=( $(filter ^features "$@" 2>/dev/null || true) ) if [ "${#specs[@]}" -eq 0 ] && [ "${#cukes[@]}" -eq 0 ]; then echo "Error: nothing to test." >&2 exit 1 fi fi if [ "${#specs[@]}" -gt 0 ]; then run rspec "${specs[@]}" fi if [ "${#cukes[@]}" -gt 0 ]; then run cucumber "${cukes[@]}" fi vcr-5.0.0/spec/000077500000000000000000000000001347305653000132465ustar00rootroot00000000000000vcr-5.0.0/spec/acceptance/000077500000000000000000000000001347305653000153345ustar00rootroot00000000000000vcr-5.0.0/spec/acceptance/concurrency_spec.rb000066400000000000000000000030011347305653000212170ustar00rootroot00000000000000require 'spec_helper' describe VCR do def recorded_content_for(name) VCR.cassette_persisters[:file_system]["#{name}.yml"].to_s end context 'when used in a multithreaded environment with an around_http_request', :with_monkey_patches => :excon do def preload_yaml_serializer_to_avoid_circular_require_warning_race_condition VCR.cassette_serializers[:yaml] end before { preload_yaml_serializer_to_avoid_circular_require_warning_race_condition } it 'can use a cassette in an #around_http_request hook' do VCR.configure do |vcr| vcr.around_http_request do |req| VCR.use_cassette(req.parsed_uri.path, &req) end end threads = 50.times.map do Thread.start do Excon.get "http://localhost:#{VCR::SinatraApp.port}/search?q=thread" end end Excon.get "http://localhost:#{VCR::SinatraApp.port}/foo" threads.each(&:join) expect(recorded_content_for("search") + recorded_content_for("foo")).to include("query: thread", "FOO!") end end context 'when used in a multithreaded environment with a cassette', :with_monkey_patches => :excon do it 'properly stubs threaded requests' do VCR.use_cassette('/foo') do threads = 50.times.map do Thread.start do Excon.get "http://localhost:#{VCR::SinatraApp.port}/foo" end end threads.each(&:join) end expect( recorded_content_for("foo")).to include("FOO!") end end end vcr-5.0.0/spec/acceptance/threading_spec.rb000066400000000000000000000020201347305653000206320ustar00rootroot00000000000000require 'spec_helper' describe VCR do context 'when used in a multithreaded environment', :with_monkey_patches => :excon do def preload_yaml_serializer_to_avoid_circular_require_warning_race_condition VCR.cassette_serializers[:yaml] end before { preload_yaml_serializer_to_avoid_circular_require_warning_race_condition } def recorded_content_for(name) VCR.cassette_persisters[:file_system]["#{name}.yml"].to_s end it 'can use a cassette in an #around_http_request hook' do VCR.configure do |vcr| vcr.around_http_request do |req| VCR.use_cassette(req.parsed_uri.path, &req) end end thread = Thread.start do Excon.get "http://localhost:#{VCR::SinatraApp.port}/search?q=thread" end Excon.get "http://localhost:#{VCR::SinatraApp.port}/foo", :response_block => Proc.new { thread.join } expect(recorded_content_for("search") + recorded_content_for("foo")).to include("query: thread", "FOO!") end end end vcr-5.0.0/spec/fixtures/000077500000000000000000000000001347305653000151175ustar00rootroot00000000000000vcr-5.0.0/spec/fixtures/cassette_spec/000077500000000000000000000000001347305653000177445ustar00rootroot00000000000000vcr-5.0.0/spec/fixtures/cassette_spec/1_x_cassette.yml000066400000000000000000000054501347305653000230550ustar00rootroot00000000000000--- - !ruby/struct:VCR::HTTPInteraction request: !ruby/struct:VCR::Request method: :get uri: http://example.com:80/ body: headers: response: !ruby/struct:VCR::Response status: !ruby/struct:VCR::ResponseStatus code: 200 message: OK headers: last-modified: - Tue, 15 Nov 2005 13:24:10 GMT connection: - Keep-Alive date: - Thu, 25 Feb 2010 07:52:38 GMT content-type: - text/html; charset=UTF-8 etag: - "\"24ec5-1b6-4059a80bfd280\"" server: - Apache/2.2.3 (CentOS) content-length: - "438" age: - "3009" accept-ranges: - bytes body: | Example Web Page

You have reached this web page by typing "example.com", "example.net", or "example.org" into your web browser.

These domain names are reserved for use in documentation and are not available for registration. See RFC 2606, Section 3.

http_version: "1.1" - !ruby/struct:VCR::HTTPInteraction request: !ruby/struct:VCR::Request method: :get uri: http://example.com:80/foo body: headers: response: !ruby/struct:VCR::Response status: !ruby/struct:VCR::ResponseStatus code: 404 message: Not Found headers: connection: - close content-type: - text/html; charset=iso-8859-1 date: - Thu, 25 Feb 2010 07:52:38 GMT server: - Apache/2.2.3 (CentOS) content-length: - "277" body: | 404 Not Found

Not Found

The requested URL /foo was not found on this server.


Apache/2.2.3 (CentOS) Server at example.com Port 80
http_version: "1.1" - !ruby/struct:VCR::HTTPInteraction request: !ruby/struct:VCR::Request method: :get uri: http://example.com:80/ body: headers: response: !ruby/struct:VCR::Response status: !ruby/struct:VCR::ResponseStatus code: 200 message: OK headers: last-modified: - Tue, 15 Nov 2005 13:24:10 GMT connection: - Keep-Alive date: - Thu, 25 Feb 2010 07:52:38 GMT content-type: - text/html; charset=UTF-8 etag: - "\"24ec5-1b6-4059a80bfd280\"" server: - Apache/2.2.3 (CentOS) content-length: - "438" age: - "3009" accept-ranges: - bytes body: Another example.com response http_version: "1.1" vcr-5.0.0/spec/fixtures/cassette_spec/empty.yml000066400000000000000000000000001347305653000216130ustar00rootroot00000000000000vcr-5.0.0/spec/fixtures/cassette_spec/example.yml000066400000000000000000000050551347305653000221270ustar00rootroot00000000000000--- http_interactions: - request: method: get uri: http://example.com/ body: '' headers: {} response: status: code: 200 message: OK headers: Last-Modified: - Tue, 15 Nov 2005 13:24:10 GMT Connection: - Keep-Alive Date: - Thu, 25 Feb 2010 07:52:38 GMT Content-Type: - text/html; charset=UTF-8 Etag: - ! '"24ec5-1b6-4059a80bfd280"' Server: - Apache/2.2.3 (CentOS) Content-Length: - '438' Age: - '3009' Accept-Ranges: - bytes body: ! "\n\n Example Web Page\n \n \n

You have reached this web page by typing "example.com",\n"example.net",\n \ or "example.org" into your web browser.

\n

These domain names are reserved for use in documentation and are not available \n for registration. See RFC \n 2606, Section 3.

\n\n\n" http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: get uri: http://example.com/foo body: '' headers: {} response: status: code: 404 message: Not Found headers: Connection: - close Content-Type: - text/html; charset=iso-8859-1 Date: - Thu, 25 Feb 2010 07:52:38 GMT Server: - Apache/2.2.3 (CentOS) Content-Length: - '277' body: ! ' 404 Not Found

Not Found

The requested URL /foo was not found on this server.


Apache/2.2.3 (CentOS) Server at example.com Port 80
' http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: get uri: http://example.com/ body: '' headers: {} response: status: code: 200 message: OK headers: Last-Modified: - Tue, 15 Nov 2005 13:24:10 GMT Connection: - Keep-Alive Date: - Thu, 25 Feb 2010 07:52:38 GMT Content-Type: - text/html; charset=UTF-8 Etag: - ! '"24ec5-1b6-4059a80bfd280"' Server: - Apache/2.2.3 (CentOS) Content-Length: - '438' Age: - '3009' Accept-Ranges: - bytes body: Another example.com response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT recorded_with: VCR 2.0.0 vcr-5.0.0/spec/fixtures/cassette_spec/with_localhost_requests.yml000066400000000000000000000043651347305653000254550ustar00rootroot00000000000000--- http_interactions: - request: method: get uri: http://localhost/ body: '' headers: {} response: status: code: 200 message: OK headers: Etag: - ! '"24ec5-1b6-4059a80bfd280"' Last-Modified: - Tue, 15 Nov 2005 13:24:10 GMT Connection: - Keep-Alive Content-Type: - text/html; charset=UTF-8 Date: - Thu, 25 Feb 2010 07:53:51 GMT Server: - Apache/2.2.3 (CentOS) Content-Length: - '438' Age: - '9260' Accept-Ranges: - bytes body: Localhost response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: get uri: http://127.0.0.1/ body: '' headers: {} response: status: code: 404 message: Not Found headers: Content-Type: - text/html; charset=iso-8859-1 Connection: - close Server: - Apache/2.2.3 (CentOS) Date: - Thu, 25 Feb 2010 07:53:52 GMT Content-Length: - '277' body: 127.0.0.1 response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: get uri: http://0.0.0.0/ body: '' headers: {} response: status: code: 404 message: Not Found headers: Content-Type: - text/html; charset=iso-8859-1 Connection: - close Server: - Apache/2.2.3 (CentOS) Date: - Thu, 25 Feb 2010 07:53:52 GMT Content-Length: - '277' body: 127.0.0.1 response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: get uri: http://example.com/ body: '' headers: {} response: status: code: 200 message: OK headers: Etag: - ! '"24ec5-1b6-4059a80bfd280"' Last-Modified: - Tue, 15 Nov 2005 13:24:10 GMT Connection: - Keep-Alive Content-Type: - text/html; charset=UTF-8 Date: - Thu, 25 Feb 2010 07:53:51 GMT Server: - Apache/2.2.3 (CentOS) Content-Length: - '438' Age: - '9260' Accept-Ranges: - bytes body: example.com response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT recorded_with: VCR 1.11.3 vcr-5.0.0/spec/fixtures/fake_example_responses.yml000066400000000000000000000044761347305653000223770ustar00rootroot00000000000000--- http_interactions: - request: method: get uri: http://example.com/foo body: '' headers: {} response: status: code: 200 message: OK headers: Last-Modified: - Tue, 15 Nov 2005 13:24:10 GMT Connection: - Keep-Alive Etag: - ! '"24ec5-1b6-4059a80bfd280"' Content-Type: - text/html; charset=UTF-8 Date: - Wed, 31 Mar 2010 02:43:23 GMT Server: - Apache/2.2.3 (CentOS) Content-Length: - '438' Age: - '3285' Accept-Ranges: - bytes body: example.com get response 1 with path=foo http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: get uri: http://example.com/foo body: '' headers: {} response: status: code: 200 message: OK headers: Last-Modified: - Tue, 15 Nov 2005 13:24:10 GMT Connection: - Keep-Alive Etag: - ! '"24ec5-1b6-4059a80bfd280"' Content-Type: - text/html; charset=UTF-8 Date: - Wed, 31 Mar 2010 02:43:23 GMT Server: - Apache/2.2.3 (CentOS) Content-Length: - '438' Age: - '3285' Accept-Ranges: - bytes body: example.com get response 2 with path=foo http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: post uri: http://example.com/ body: '' headers: {} response: status: code: 200 message: OK headers: Last-Modified: - Tue, 15 Nov 2005 13:24:10 GMT Connection: - close Etag: - ! '"24ec5-1b6-4059a80bfd280"' Content-Type: - text/html; charset=UTF-8 Date: - Wed, 31 Mar 2010 02:43:26 GMT Server: - Apache/2.2.3 (CentOS) Content-Length: - '438' Accept-Ranges: - bytes body: example.com post response with id=3 http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: get uri: http://example.com/two_set_cookie_headers body: '' headers: {} response: status: code: 200 message: OK headers: Set-Cookie: - bar=bazz - foo=bar body: this respons has two set-cookie headers http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT recorded_with: VCR 1.11.3 vcr-5.0.0/spec/fixtures/match_requests_on.yml000066400000000000000000000073711347305653000213750ustar00rootroot00000000000000--- http_interactions: - request: method: post uri: http://example.com/method body: '' headers: {} response: status: code: 200 message: OK headers: Etag: - ! '"24ec5-1b6-4059a80bfd280"' body: post method response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: get uri: http://example.com/method body: '' headers: {} response: status: code: 200 message: OK headers: Etag: - ! '"24ec5-1b6-4059a80bfd280"' body: get method response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: post uri: http://example1.com/host body: '' headers: {} response: status: code: 200 message: OK headers: Etag: - ! '"24ec5-1b6-4059a80bfd280"' body: example1.com host response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: post uri: http://example2.com/host body: '' headers: {} response: status: code: 200 message: OK headers: Etag: - ! '"24ec5-1b6-4059a80bfd280"' body: example2.com host response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: post uri: http://example.com/path1 body: '' headers: {} response: status: code: 200 message: OK headers: Etag: - ! '"24ec5-1b6-4059a80bfd280"' body: path1 response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: post uri: http://example.com/path2 body: '' headers: {} response: status: code: 200 message: OK headers: Etag: - ! '"24ec5-1b6-4059a80bfd280"' body: path2 response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: post uri: http://example.com/uri1 body: '' headers: {} response: status: code: 200 message: OK headers: Etag: - ! '"24ec5-1b6-4059a80bfd280"' body: uri1 response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: post uri: http://example.com/uri2 body: '' headers: {} response: status: code: 200 message: OK headers: Etag: - ! '"24ec5-1b6-4059a80bfd280"' body: uri2 response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: post uri: http://example.com/ body: param=val1 headers: {} response: status: code: 200 message: OK headers: Etag: - ! '"24ec5-1b6-4059a80bfd280"' body: val1 body response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: post uri: http://example.com/ body: param=val2 headers: {} response: status: code: 200 message: OK headers: Etag: - ! '"24ec5-1b6-4059a80bfd280"' body: val2 body response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: post uri: http://example.com/ body: '' headers: X-Http-Header1: - val1 response: status: code: 200 message: OK headers: Etag: - ! '"24ec5-1b6-4059a80bfd280"' body: val1 header response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT - request: method: post uri: http://example.com/ body: '' headers: X-Http-Header1: - val2 response: status: code: 200 message: OK headers: Etag: - ! '"24ec5-1b6-4059a80bfd280"' body: val2 header response http_version: '1.1' recorded_at: Tue, 01 Nov 2011 04:49:54 GMT recorded_with: VCR 1.11.3 vcr-5.0.0/spec/lib/000077500000000000000000000000001347305653000140145ustar00rootroot00000000000000vcr-5.0.0/spec/lib/vcr/000077500000000000000000000000001347305653000146065ustar00rootroot00000000000000vcr-5.0.0/spec/lib/vcr/cassette/000077500000000000000000000000001347305653000164215ustar00rootroot00000000000000vcr-5.0.0/spec/lib/vcr/cassette/erb_renderer_spec.rb000066400000000000000000000036241347305653000224230ustar00rootroot00000000000000require 'spec_helper' describe VCR::Cassette::ERBRenderer do describe '#render' do def render(*args) described_class.new(*args).render end let(:no_vars_content) { '<%= 3 + 4 %>. Some ERB' } let(:vars_content) { '<%= var1 %>. ERB with Vars! <%= var2 %>' } context 'when ERB is disabled' do it 'returns the given template' do expect(render(no_vars_content, false)).to eq(no_vars_content) expect(render(no_vars_content, nil)).to eq(no_vars_content) end end context 'when ERB is enabled but no variables are passed' do it 'renders the file content as ERB' do expect(render(no_vars_content, true)).to eq("7. Some ERB") end it 'raises an appropriate error when the ERB template needs variables' do expect { render(vars_content, true, "vars") }.to raise_error(VCR::Errors::MissingERBVariableError, %{The ERB in the vars cassette file references undefined variable var1. } + %{Pass it to the cassette using :erb => #{ {:var1=>"some value"}.inspect }.} ) end it 'gracefully handles the template being nil' do expect(render(nil, true)).to be_nil end end context 'when ERB is enabled and variables are passed' do it 'renders the file content as ERB with the passed variables' do expect(render(vars_content, :var1 => 'foo', :var2 => 'bar')).to eq('foo. ERB with Vars! bar') end it 'raises an appropriate error when one or more of the needed variables are not passed' do expect { render(vars_content, { :var1 => 'foo' }, "vars") }.to raise_error(VCR::Errors::MissingERBVariableError, %{The ERB in the vars cassette file references undefined variable var2. } + %{Pass it to the cassette using :erb => #{ {:var1 => "foo", :var2 => "some value"}.inspect }.} ) end end end end vcr-5.0.0/spec/lib/vcr/cassette/http_interaction_list_spec.rb000066400000000000000000000255351347305653000244030ustar00rootroot00000000000000require 'vcr/util/logger' require 'vcr/cassette/http_interaction_list' require 'vcr/request_matcher_registry' require 'vcr/structs' require 'support/configuration_stubbing' module VCR class Cassette describe HTTPInteractionList do include_context "configuration stubbing" ::RSpec::Matchers.define :respond_with do |expected| match { |a| expected.nil? ? a.nil? : a.body == expected } end before(:each) do allow(VCR).to receive(:request_matchers).and_return(VCR::RequestMatcherRegistry.new) allow(config).to receive(:logger).and_return(double.as_null_object) end def request_with(values) VCR::Request.new.tap do |request| values.each do |name, value| request.send("#{name}=", value) end end end def response(body) VCR::Response.new.tap do |r| r.body = body r.status = VCR::ResponseStatus.new(200) end end def interaction(body, request_values) VCR::HTTPInteraction.new \ request_with(request_values), response(body) end let(:original_list_array) do [ interaction('put response', :method => :put), interaction('post response 1', :method => :post), interaction('post response 2', :method => :post) ] end let(:allow_playback_repeats) { false } # the default let(:list) { HTTPInteractionList.new(original_list_array, [:method], allow_playback_repeats) } describe "#has_used_interaction_matching?" do it 'returns false when no interactions have been used' do expect(list).not_to have_used_interaction_matching(request_with(:method => :put)) end it 'returns true when there is a matching used interaction (even if there is also an unused one that matches)' do list.response_for(request_with(:method => :post)) expect(list).to have_used_interaction_matching(request_with(:method => :post)) end it 'returns false when none of the used interactions match' do list.response_for(request_with(:method => :put)) expect(list).not_to have_used_interaction_matching(request_with(:method => :post)) end end describe "#remaining_unused_interaction_count" do it 'returns the number of unused interactions' do expect(list.remaining_unused_interaction_count).to eq(3) list.response_for(request_with(:method => :get)) expect(list.remaining_unused_interaction_count).to eq(3) list.response_for(request_with(:method => :put)) expect(list.remaining_unused_interaction_count).to eq(2) list.response_for(request_with(:method => :put)) expect(list.remaining_unused_interaction_count).to eq(2) list.response_for(request_with(:method => :post)) expect(list.remaining_unused_interaction_count).to eq(1) list.response_for(request_with(:method => :post)) expect(list.remaining_unused_interaction_count).to eq(0) list.response_for(request_with(:method => :post)) expect(list.remaining_unused_interaction_count).to eq(0) end end describe "#assert_no_unused_interactions?" do it 'should raise a SkippedHTTPRequestError when there are unused interactions left' do expect { list.assert_no_unused_interactions! }.to raise_error(Errors::UnusedHTTPInteractionError) list.response_for(request_with(:method => :put)) expect { list.assert_no_unused_interactions! }.to raise_error(Errors::UnusedHTTPInteractionError) end it 'should raise nothing when there are no unused interactions left' do [:put, :post, :post].each do |method| list.response_for(request_with(:method => method)) end expect { list.assert_no_unused_interactions! }.not_to raise_error end context 'when the null logger is in use' do before { allow(config).to receive(:logger).and_return(Logger::Null) } it 'includes formatted request details in the error message' do expect { list.assert_no_unused_interactions! }.to raise_error(/\[put/) end it 'includes formatted response details in the error message' do expect { list.assert_no_unused_interactions! }.to raise_error(/\[200 "put response"\]/) end end end describe "has_interaction_matching?" do it 'returns false when the list is empty' do expect(HTTPInteractionList.new([], [:method])).not_to have_interaction_matching(double) end it 'returns false when there is no matching interaction' do expect(list).not_to have_interaction_matching(request_with(:method => :get)) end it 'returns true when there is a matching interaction' do expect(list).to have_interaction_matching(request_with(:method => :post)) end it 'does not consume the interactions when they match' do expect(list).to have_interaction_matching(request_with(:method => :post)) expect(list.remaining_unused_interaction_count).to eq(3) expect(list).to have_interaction_matching(request_with(:method => :post)) expect(list.remaining_unused_interaction_count).to eq(3) end it 'invokes each matcher block to find the matching interaction' do VCR.request_matchers.register(:foo) { |r1, r2| true } VCR.request_matchers.register(:bar) { |r1, r2| true } calls = 0 VCR.request_matchers.register(:baz) { |r1, r2| calls += 1; calls == 2 } list = HTTPInteractionList.new([ interaction('response', :method => :put) ], [:foo, :bar, :baz]) expect(list).not_to have_interaction_matching(request_with(:method => :post)) expect(list).to have_interaction_matching(request_with(:method => :post)) end it "delegates to the parent list when it can't find a matching interaction" do parent_list = double(:has_interaction_matching? => true) expect(HTTPInteractionList.new( [], [:method], false, parent_list)).to have_interaction_matching(double) parent_list = double(:has_interaction_matching? => false) expect(HTTPInteractionList.new( [], [:method], false, parent_list)).not_to have_interaction_matching(double) end context 'when allow_playback_repeats is set to true' do let(:allow_playback_repeats) { true } it 'considers used interactions' do list.response_for(request_with(:method => :put)) results = 10.times.map do list.has_interaction_matching?(request_with(:method => :put)) end expect(results).to eq([true] * 10) end end context 'when allow_playback_repeats is set to false' do let(:allow_playback_repeats) { false } it 'does not consider used interactions' do list.response_for(request_with(:method => :put)) result = 10.times.map do list.has_interaction_matching?(request_with(:method => :put)) end expect(result).to eq([false] * 10) end end end describe "#response_for" do it 'returns nil when the list is empty' do expect(HTTPInteractionList.new([], [:method]).response_for(double)).to respond_with(nil) end it 'returns nil when there is no matching interaction' do response = HTTPInteractionList.new([ interaction('foo', :method => :post), interaction('foo', :method => :put) ], [:method]).response_for( request_with(:method => :get) ) expect(response).to respond_with(nil) end it 'returns the first matching interaction' do list = HTTPInteractionList.new([ interaction('put response', :method => :put), interaction('post response 1', :method => :post), interaction('post response 2', :method => :post) ], [:method]) expect(list.response_for(request_with(:method => :post))).to respond_with("post response 1") end it 'invokes each matcher block to find the matching interaction' do VCR.request_matchers.register(:foo) { |r1, r2| true } VCR.request_matchers.register(:bar) { |r1, r2| true } calls = 0 VCR.request_matchers.register(:baz) { |r1, r2| calls += 1; calls == 2 } list = HTTPInteractionList.new([ interaction('response', :method => :put) ], [:foo, :bar, :baz]) expect(list.response_for(request_with(:method => :post))).to respond_with(nil) expect(list.response_for(request_with(:method => :post))).to respond_with('response') end it "delegates to the parent list when it can't find a matching interaction" do parent_list = double(:response_for => response('parent')) result = HTTPInteractionList.new( [], [:method], false, parent_list ).response_for(double) expect(result).to respond_with('parent') end it 'consumes the first matching interaction so that it will not be used again' do expect(list.response_for(request_with(:method => :post)).body).to eq("post response 1") expect(list.response_for(request_with(:method => :post)).body).to eq("post response 2") end context 'when allow_playback_repeats is set to true' do let(:allow_playback_repeats) { true } it 'continues to return the response from the last matching interaction when there are no more' do list.response_for(request_with(:method => :post)) results = 10.times.map do response = list.response_for(request_with(:method => :post)) response ? response.body : nil end expect(results).to eq(["post response 2"] * 10) end end context 'when allow_playback_repeats is set to false' do let(:allow_playback_repeats) { false } it 'returns nil when there are no more unused interactions' do list.response_for(request_with(:method => :post)) list.response_for(request_with(:method => :post)) results = 10.times.map do list.response_for(request_with(:method => :post)) end expect(results).to eq([nil] * 10) end end it 'does not modify the original interaction array the list was initialized with' do original_dup = original_list_array.dup list.response_for(request_with(:method => :post)) expect(original_list_array).to eq original_dup end end end end end vcr-5.0.0/spec/lib/vcr/cassette/migrator_spec.rb000066400000000000000000000123431347305653000216070ustar00rootroot00000000000000require 'tmpdir' require 'vcr/cassette/migrator' require 'yaml' describe VCR::Cassette::Migrator do let(:original_contents) { <<-EOF --- - !ruby/struct:VCR::HTTPInteraction request: !ruby/struct:VCR::Request method: :get uri: http://example.com:80/foo body: headers: response: !ruby/struct:VCR::Response status: !ruby/struct:VCR::ResponseStatus code: 200 message: OK headers: content-type: - text/html;charset=utf-8 content-length: - "9" body: Hello foo http_version: "1.1" - !ruby/struct:VCR::HTTPInteraction request: !ruby/struct:VCR::Request method: :get uri: http://localhost:7777/bar body: headers: response: !ruby/struct:VCR::Response status: !ruby/struct:VCR::ResponseStatus code: 200 message: OK headers: content-type: - text/html;charset=utf-8 content-length: - "9" body: Hello bar http_version: "1.1" EOF } let(:updated_contents) { <<-EOF --- http_interactions: - request: method: get uri: http://example.com/foo body: encoding: US-ASCII string: "" headers: {} response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - "9" body: encoding: UTF-8 string: Hello foo http_version: "1.1" recorded_at: Wed, 04 May 2011 12:30:00 GMT - request: method: get uri: http://localhost:7777/bar body: encoding: US-ASCII string: "" headers: {} response: status: code: 200 message: OK headers: Content-Type: - text/html;charset=utf-8 Content-Length: - "9" body: encoding: UTF-8 string: Hello bar http_version: "1.1" recorded_at: Wed, 04 May 2011 12:30:00 GMT recorded_with: VCR #{VCR.version} EOF } let(:dir) { './tmp/migrator' } before(:each) do # ensure the directory is empty FileUtils.rm_rf dir FileUtils.mkdir_p dir end before(:each) do # the encoding won't be set on rubies that don't support it updated_contents.gsub!(/^\s+encoding:.*$/, '') end unless ''.respond_to?(:encoding) # JRuby serializes YAML with some slightly different whitespace. before(:each) do [original_contents, updated_contents].each do |contents| contents.gsub!(/^(\s+)-/, '\1 -') end updated_contents.gsub!(/^(- | )/, ' \1') end if RUBY_PLATFORM == 'java' # Use syck on all rubies for consistent results... around(:each) do |example| YAML::ENGINE.yamler = 'syck' begin example.call ensure YAML::ENGINE.yamler = 'psych' end end if defined?(YAML::ENGINE) && RUBY_VERSION.to_f < 2.0 let(:filemtime) { Time.utc(2011, 5, 4, 12, 30) } let(:out_io) { StringIO.new } let(:file_name) { File.join(dir, "example.yml") } let(:output) { out_io.rewind; out_io.read } subject { described_class.new(dir, out_io) } before(:each) do allow(File).to receive(:mtime).with(file_name).and_return(filemtime) end it 'migrates a cassette from the 1.x to 2.x format' do File.open(file_name, 'w') { |f| f.write(original_contents) } subject.migrate! expect(YAML.load_file(file_name)).to eq(YAML.load(updated_contents)) expect(output).to match(/Migrated example.yml/) end it 'ignores files that do not contain arrays' do File.open(file_name, 'w') { |f| f.write(true.to_yaml) } subject.migrate! expect(File.read(file_name)).to eq(true.to_yaml) expect(output).to match(/Ignored example.yml since it does not appear to be a valid VCR 1.x cassette/) end it 'ignores files that contain YAML arrays of other things' do File.open(file_name, 'w') { |f| f.write([{}, {}].to_yaml) } subject.migrate! expect(File.read(file_name)).to eq([{}, {}].to_yaml) expect(output).to match(/Ignored example.yml since it does not appear to be a valid VCR 1.x cassette/) end it 'ignores URIs that have sensitive data substitutions' do modified_contents = original_contents.gsub('example.com', '') File.open(file_name, 'w') { |f| f.write(modified_contents) } subject.migrate! expect(YAML.load_file(file_name)).to eq(YAML.load(updated_contents.gsub('example.com', ':80'))) end it 'ignores files that are empty' do File.open(file_name, 'w') { |f| f.write('') } subject.migrate! expect(File.read(file_name)).to eq('') expect(output).to match(/Ignored example.yml since it could not be parsed as YAML/) end shared_examples_for "ignoring invalid YAML" do it 'ignores files that cannot be parsed as valid YAML (such as ERB cassettes)' do modified_contents = original_contents.gsub(/\A---/, "---\n<% 3.times do %>") modified_contents = modified_contents.gsub(/\z/, "<% end %>") File.open(file_name, 'w') { |f| f.write(modified_contents) } subject.migrate! expect(File.read(file_name)).to eq(modified_contents) expect(output).to match(/Ignored example.yml since it could not be parsed as YAML/) end end context 'with syck' do it_behaves_like "ignoring invalid YAML" end context 'with psych' do before(:each) do YAML::ENGINE.yamler = 'psych' end it_behaves_like "ignoring invalid YAML" end if defined?(YAML::ENGINE) end vcr-5.0.0/spec/lib/vcr/cassette/persisters/000077500000000000000000000000001347305653000206245ustar00rootroot00000000000000vcr-5.0.0/spec/lib/vcr/cassette/persisters/file_system_spec.rb000066400000000000000000000061611347305653000245120ustar00rootroot00000000000000require 'spec_helper' require 'vcr/cassette/persisters/file_system' module VCR class Cassette class Persisters describe FileSystem do before { FileSystem.storage_location = VCR.configuration.cassette_library_dir } describe "#[]" do it 'reads from the given file, relative to the configured storage location' do File.open(FileSystem.storage_location + '/foo.txt', 'w') { |f| f.write('1234') } expect(FileSystem["foo.txt"]).to eq("1234") end it 'handles directories in the given file name' do FileUtils.mkdir_p FileSystem.storage_location + '/a' File.open(FileSystem.storage_location + '/a/b', 'w') { |f| f.write('1234') } expect(FileSystem["a/b"]).to eq("1234") end it 'returns nil if the file does not exist' do expect(FileSystem["non_existant_file"]).to be_nil end end describe "#[]=" do context 'with a simple file_name and binary content' do let(:file_name) { 'foo.txt' } let(:content) { SecureRandom.random_bytes(20) } let(:location) { FileSystem.storage_location + '/' + file_name } it 'writes the given file contents to the given file name' do expect(File.exist?(location)).to be false FileSystem[file_name] = content expect(File.binread(location)).to eq(content) end end it 'creates any needed intermediary directories' do expect(File.exist?(FileSystem.storage_location + '/a')).to be false FileSystem["a/b"] = "bar" expect(File.read(FileSystem.storage_location + '/a/b')).to eq("bar") end end describe "#absolute_path_to_file" do it "returns the absolute path to the given relative file based on the storage location" do expected = File.join(FileSystem.storage_location, "bar/bazz.json") expect(FileSystem.absolute_path_to_file("bar/bazz.json")).to eq(expected) end it "returns nil if the storage_location is not set" do FileSystem.storage_location = nil expect(FileSystem.absolute_path_to_file("bar/bazz.json")).to be_nil end it "sanitizes the file name" do expected = File.join(FileSystem.storage_location, "_t_i-t_1_2_f_n.json") expect(FileSystem.absolute_path_to_file("\nt \t! i-t_1.2_f n.json")).to eq(expected) expected = File.join(FileSystem.storage_location, "a_1/b") expect(FileSystem.absolute_path_to_file("a 1/b")).to eq(expected) expected = File.join(FileSystem.storage_location, "\u842c\u570b\u78bc") expect(FileSystem.absolute_path_to_file("\u842c\u570b\u78bc")).to eq(expected) end it 'handles files with no extensions (even when there is a dot in the path)' do expected = File.join(FileSystem.storage_location, "/foo_bar/baz_qux") expect(FileSystem.absolute_path_to_file("/foo.bar/baz qux")).to eq(expected) end end end end end end vcr-5.0.0/spec/lib/vcr/cassette/persisters_spec.rb000066400000000000000000000021701347305653000221630ustar00rootroot00000000000000require 'vcr/cassette/persisters' module VCR class Cassette describe Persisters do describe "#[]=" do context 'when there is already a persister registered for the given name' do before(:each) do subject[:foo] = :old_persister allow(subject).to receive :warn end it 'overrides the existing persister' do subject[:foo] = :new_persister expect(subject[:foo]).to be(:new_persister) end it 'warns that there is a name collision' do expect(subject).to receive(:warn).with( /WARNING: There is already a VCR cassette persister registered for :foo\. Overriding it/ ) subject[:foo] = :new_persister end end end describe "#[]" do it 'raises an error when given an unrecognized persister name' do expect { subject[:foo] }.to raise_error(ArgumentError) end it 'returns the named persister' do expect(subject[:file_system]).to be(VCR::Cassette::Persisters::FileSystem) end end end end end vcr-5.0.0/spec/lib/vcr/cassette/serializers_spec.rb000066400000000000000000000145311347305653000223200ustar00rootroot00000000000000require 'support/ruby_interpreter' require 'vcr/cassette/serializers' require 'multi_json' begin require 'psych' # ensure psych is loaded for these tests if its available rescue LoadError end module VCR class Cassette describe Serializers do shared_examples_for "encoding error handling" do |name, error_class| context "the #{name} serializer" do it 'appends info about the :preserve_exact_body_bytes option to the error' do expect { result = serializer.serialize("a" => string) serializer.deserialize(result) }.to raise_error(error_class, /preserve_exact_body_bytes/) end unless (RUBY_INTERPRETER == :rubinius && RUBY_VERSION =~ /^1.9/) end end shared_examples_for "a serializer" do |name, file_extension, lazily_loaded| let(:serializer) { subject[name] } context "the #{name} serializer" do it 'lazily loads the serializer' do serializers = subject.instance_variable_get(:@serializers) expect(serializers).not_to have_key(name) expect(subject[name]).not_to be_nil expect(serializers).to have_key(name) end if lazily_loaded it "returns '#{file_extension}' as the file extension" do expect(serializer.file_extension).to eq(file_extension) end it "can serialize and deserialize a hash" do hash = { "a" => 7, "nested" => { "hash" => [1, 2, 3] }} serialized = serializer.serialize(hash) expect(serialized).not_to eq(hash) expect(serialized).to be_a(String) deserialized = serializer.deserialize(serialized) expect(deserialized).to eq(hash) end end end it_behaves_like "a serializer", :yaml, "yml", :lazily_loaded do it_behaves_like "encoding error handling", :yaml, ArgumentError do let(:string) { "\xFA".force_encoding("UTF-8") } before { ::YAML::ENGINE.yamler = 'psych' if defined?(::YAML::ENGINE) } end if ''.respond_to?(:encoding) end it_behaves_like "a serializer", :syck, "yml", :lazily_loaded do it_behaves_like "encoding error handling", :syck, ArgumentError do let(:string) { "\xFA".force_encoding("UTF-8") } end if ''.respond_to?(:encoding) end it_behaves_like "a serializer", :psych, "yml", :lazily_loaded do it_behaves_like "encoding error handling", :psych, ArgumentError do let(:string) { "\xFA".force_encoding("UTF-8") } end if ''.respond_to?(:encoding) end if RUBY_VERSION =~ /1.9/ it_behaves_like "a serializer", :compressed, "zz", :lazily_loaded do it_behaves_like "encoding error handling", :compressed, ArgumentError do let(:string) { "\xFA".force_encoding("UTF-8") } end if ''.respond_to?(:encoding) end it_behaves_like "a serializer", :json, "json", :lazily_loaded do engines = {} if RUBY_INTERPRETER == :jruby # don't test yajl on jruby else engines[:yajl] = MultiJson::LoadError end if RUBY_VERSION =~ /1.9/ engines[:json_gem] = EncodingError # Disable json_pure for now due to this bug: # https://github.com/flori/json/issues/186 # engines[:json_pure] = EncodingError end engines.each do |engine, error| context "when MultiJson is configured to use #{engine.inspect}", :unless => (RUBY_INTERPRETER == :jruby) do before { MultiJson.engine = engine } it_behaves_like "encoding error handling", :json, error do let(:string) { "\xFA" } end end end end context "a custom :ruby serializer" do let(:custom_serializer) do Object.new.tap do |obj| def obj.file_extension "rb" end def obj.serialize(hash) hash.inspect end def obj.deserialize(string) eval(string) end end end before(:each) do subject[:ruby] = custom_serializer end it_behaves_like "a serializer", :ruby, "rb", false end describe "#[]=" do context 'when there is already a serializer registered for the given name' do before(:each) do subject[:foo] = :old_serializer allow(subject).to receive :warn end it 'overrides the existing serializer' do subject[:foo] = :new_serializer expect(subject[:foo]).to be(:new_serializer) end it 'warns that there is a name collision' do expect(subject).to receive(:warn).with( /WARNING: There is already a VCR cassette serializer registered for :foo\. Overriding it/ ) subject[:foo] = :new_serializer end end end describe "#[]" do it 'raises an error when given an unrecognized serializer name' do expect { subject[:foo] }.to raise_error(ArgumentError) end it 'returns the named serializer' do expect(subject[:yaml]).to be(VCR::Cassette::Serializers::YAML) end end # see https://gist.github.com/815769 problematic_syck_string = "1\n \n2" describe "psych serializer" do it 'serializes things using pysch even if syck is configured as the default YAML engine' do ::YAML::ENGINE.yamler = 'syck' serialized = subject[:psych].serialize(problematic_syck_string) expect(subject[:psych].deserialize(serialized)).to eq(problematic_syck_string) end if defined?(::Psych) && RUBY_VERSION.to_f < 2.0 it 'raises an error if psych cannot be loaded' do expect { subject[:psych] }.to raise_error(LoadError) end unless defined?(::Psych) end describe "syck serializer" do it 'forcibly serializes things using syck even if psych is the currently configured YAML engine' do ::YAML::ENGINE.yamler = 'psych' serialized = subject[:syck].serialize(problematic_syck_string) expect(subject[:syck].deserialize(serialized)).not_to eq(problematic_syck_string) end if defined?(::Psych) && (RUBY_INTERPRETER != :jruby) && (RUBY_VERSION.to_f < 2.0) end end end end vcr-5.0.0/spec/lib/vcr/cassette_spec.rb000066400000000000000000000666301347305653000177730ustar00rootroot00000000000000require 'spec_helper' describe VCR::Cassette do def http_interaction request = VCR::Request.new(:get) response = VCR::Response.new response.status = VCR::ResponseStatus.new VCR::HTTPInteraction.new(request, response).tap { |i| yield i if block_given? } end def stub_old_interactions(interactions) VCR.configuration.cassette_library_dir = "#{VCR::SPEC_ROOT}/fixtures/cassette_spec" hashes = interactions.map(&:to_hash) allow(VCR.cassette_serializers[:yaml]).to receive(:deserialize).and_return({ 'http_interactions' => hashes }) allow(VCR::HTTPInteraction).to receive(:from_hash) do |hash| interactions[hashes.index(hash)] end end describe '#file' do it 'delegates the file resolution to the FileSystem persister' do fs = VCR::Cassette::Persisters::FileSystem expect(fs).to respond_to(:absolute_path_to_file).with(1).argument expect(fs).to receive(:absolute_path_to_file).with("cassette name.yml") { "f.yml" } expect(VCR::Cassette.new("cassette name").file).to eq("f.yml") end it 'raises a NotImplementedError when a different persister is used' do VCR.cassette_persisters[:a] = double cassette = VCR::Cassette.new("f", :persist_with => :a) expect { cassette.file }.to raise_error(NotImplementedError) end end describe '#tags' do it 'returns a blank array if no tag has been set' do expect(VCR::Cassette.new("name").tags).to eq([]) end it 'converts a single :tag to an array' do expect(VCR::Cassette.new("name", :tag => :foo).tags).to eq([:foo]) end it 'accepts an array as the :tags option' do expect(VCR::Cassette.new("name", :tags => [:foo]).tags).to eq([:foo]) end end describe '#record_http_interaction' do let(:the_interaction) { double(:request => double(:method => :get).as_null_object).as_null_object } it 'adds the interaction to #new_recorded_interactions' do cassette = VCR::Cassette.new(:test_cassette) expect(cassette.new_recorded_interactions).to eq([]) cassette.record_http_interaction(the_interaction) expect(cassette.new_recorded_interactions).to eq([the_interaction]) end end describe "#serializable_hash" do subject { VCR::Cassette.new("foo") } let(:interaction_1) { http_interaction { |i| i.request.body = 'req body 1'; i.response.body = 'res body 1' } } let(:interaction_2) { http_interaction { |i| i.request.body = 'req body 2'; i.response.body = 'res body 2' } } let(:interactions) { [interaction_1, interaction_2] } before(:each) do interactions.each do |i| subject.record_http_interaction(i) end end let(:metadata) { subject.serializable_hash.reject { |k,v| k == "http_interactions" } } it 'includes the hash form of all recorded interactions' do hash_1 = interaction_1.to_hash hash_2 = interaction_2.to_hash expect(subject.serializable_hash).to include('http_interactions' => [hash_1, hash_2]) end it 'includes additional metadata about the cassette' do expect(metadata).to eq("recorded_with" => "VCR #{VCR.version}") end it 'does not allow the interactions to be mutated by configured hooks' do VCR.configure do |c| c.define_cassette_placeholder('') { 'body' } end expect { subject.serializable_hash }.not_to change { interaction_1.response.body } end describe 'clean_outdated_http_interactions' do before(:each) do subject.instance_variable_set(:@clean_outdated_http_interactions, true) subject.instance_variable_set(:@previously_recorded_interactions, subject.instance_variable_get(:@new_recorded_interactions)) subject.instance_variable_set(:@new_recorded_interactions, []) end let(:interaction_hashes) { [interaction_1, interaction_2].map(&:to_hash) } it "returns all interactions if re_record_interval is not set" do expect(subject.serializable_hash).to include('http_interactions' => interaction_hashes) end it "returns all interactions if they are not outdated" do subject.instance_variable_set(:@re_record_interval, 100) expect(subject.serializable_hash).to include('http_interactions' => interaction_hashes) end it "rejects outdated interactions" do subject.instance_variable_set(:@re_record_interval, 100) allow(Time).to receive(:now).and_return(Time.now + 105) expect(subject.serializable_hash['http_interactions']).to be_empty end end end describe "#recording?" do [:all, :new_episodes].each do |mode| it "returns true when the record mode is :#{mode}" do cassette = VCR::Cassette.new("foo", :record => mode) expect(cassette).to be_recording end end it "returns false when the record mode is :none" do cassette = VCR::Cassette.new("foo", :record => :none) expect(cassette).not_to be_recording end context 'when the record mode is :once' do before(:each) do VCR.configuration.cassette_library_dir = "#{VCR::SPEC_ROOT}/fixtures/cassette_spec" end it 'returns false when there is an existing cassette file with content' do cassette = VCR::Cassette.new("example", :record => :once) expect(::File).to exist(cassette.file) expect(::File.size?(cassette.file)).to be_truthy expect(cassette).not_to be_recording end it 'returns true when there is an empty existing cassette file' do cassette = VCR::Cassette.new("empty", :record => :once) expect(::File).to exist(cassette.file) expect(::File.size?(cassette.file)).to be_falsey expect(cassette).to be_recording end it 'returns true when there is no existing cassette file' do cassette = VCR::Cassette.new("non_existant_file", :record => :once) expect(::File).not_to exist(cassette.file) expect(cassette).to be_recording end end end describe '#match_requests_on' do before(:each) { VCR.configuration.default_cassette_options.merge!(:match_requests_on => [:uri, :method]) } it "returns the provided options" do c = VCR::Cassette.new('example', :match_requests_on => [:uri]) expect(c.match_requests_on).to eq([:uri]) end it "returns a the default #match_requests_on when it has not been specified for the cassette" do c = VCR::Cassette.new('example') expect(c.match_requests_on).to eq([:uri, :method]) end end describe "reading the file from disk" do let(:empty_cassette_yaml) { YAML.dump("http_interactions" => []) } it 'optionally renders the file as ERB using the ERBRenderer' do allow(VCR::Cassette::Persisters::FileSystem).to receive(:[]).and_return(empty_cassette_yaml) expect(VCR::Cassette::ERBRenderer).to receive(:new).with( empty_cassette_yaml, anything, "foo" ).and_return(double('renderer', :render => empty_cassette_yaml)) VCR::Cassette.new('foo', :record => :new_episodes).http_interactions end [true, false, nil, { }].each do |erb| it "passes #{erb.inspect} to the VCR::Cassette::ERBRenderer when given as the :erb option" do # test that it overrides the default VCR.configuration.default_cassette_options = { :erb => true } expect(VCR::Cassette::ERBRenderer).to receive(:new).with( anything, erb, anything ).and_return(double('renderer', :render => empty_cassette_yaml)) VCR::Cassette.new('foo', :record => :new_episodes, :erb => erb).http_interactions end it "passes #{erb.inspect} to the VCR::Cassette::ERBRenderer when it is the default :erb option and none is given" do VCR.configuration.default_cassette_options = { :erb => erb } expect(VCR::Cassette::ERBRenderer).to receive(:new).with( anything, erb, anything ).and_return(double('renderer', :render => empty_cassette_yaml)) VCR::Cassette.new('foo', :record => :new_episodes).http_interactions end end it 'raises a friendly error when the cassette file is in the old VCR 1.x format' do VCR.configuration.cassette_library_dir = 'spec/fixtures/cassette_spec' expect { VCR::Cassette.new('1_x_cassette').http_interactions }.to raise_error(VCR::Errors::InvalidCassetteFormatError) end end describe '.new' do it "raises an error if given an invalid record mode" do expect { VCR::Cassette.new(:test, :record => :not_a_record_mode) }.to raise_error(ArgumentError) end it 'raises an error if given invalid options' do expect { VCR::Cassette.new(:test, :invalid => :option) }.to raise_error(ArgumentError) end it 'does not raise an error in the case of an empty file' do VCR.configuration.cassette_library_dir = "#{VCR::SPEC_ROOT}/fixtures/cassette_spec" expect(VCR::Cassette.new('empty', :record => :none).send(:previously_recorded_interactions)).to eq([]) end let(:custom_persister) { double("custom persister") } it 'reads from the configured persister' do VCR.configuration.cassette_library_dir = nil VCR.cassette_persisters[:foo] = custom_persister expect(custom_persister).to receive(:[]).with("abc.yml") { "" } VCR::Cassette.new("abc", :persist_with => :foo).http_interactions end VCR::Cassette::VALID_RECORD_MODES.each do |record_mode| stub_requests = (record_mode != :all) context "when VCR.configuration.default_cassette_options[:record] is :#{record_mode}" do before(:each) { VCR.configuration.default_cassette_options = { :record => record_mode } } it "defaults the record mode to #{record_mode} when VCR.configuration.default_cassette_options[:record] is #{record_mode}" do cassette = VCR::Cassette.new(:test) expect(cassette.record_mode).to eq(record_mode) end end context "when :#{record_mode} is passed as the record option" do unless record_mode == :all let(:interaction_1) { http_interaction { |i| i.request.uri = 'http://example.com/foo' } } let(:interaction_2) { http_interaction { |i| i.request.uri = 'http://example.com/bar' } } let(:interactions) { [interaction_1, interaction_2] } it 'updates the content_length headers when given :update_content_length_header => true' do stub_old_interactions(interactions) expect(interaction_1.response).to receive(:update_content_length_header) expect(interaction_2.response).to receive(:update_content_length_header) VCR::Cassette.new('example', :record => record_mode, :update_content_length_header => true).http_interactions end [nil, false].each do |val| it "does not update the content_lenth headers when given :update_content_length_header => #{val.inspect}" do stub_old_interactions(interactions) expect(interaction_1.response).not_to receive(:update_content_length_header) expect(interaction_2.response).not_to receive(:update_content_length_header) VCR::Cassette.new('example', :record => record_mode, :update_content_length_header => val).http_interactions end end context "and re_record_interval is 7.days" do let(:file_name) { ::File.join(VCR.configuration.cassette_library_dir, "cassette_name.yml") } subject { VCR::Cassette.new(::File.basename(file_name).gsub('.yml', ''), :record => record_mode, :re_record_interval => 7.days) } context 'when the cassette file does not exist' do before(:each) { allow(::File).to receive(:exist?).with(file_name).and_return(false) } it "has :#{record_mode} for the record mode" do expect(subject.record_mode).to eq(record_mode) end end context 'when the cassette file does exist' do before(:each) do interactions = timestamps.map do |ts| http_interaction { |i| i.recorded_at = ts }.to_hash end yaml = YAML.dump("http_interactions" => interactions) allow(::File).to receive(:exist?).with(file_name).and_return(true) allow(::File).to receive(:size?).with(file_name).and_return(true) allow(::File).to receive(:binread).with(file_name).and_return(yaml) end context 'and the earliest recorded interaction was recorded less than 7 days ago' do let(:timestamps) do [ Time.now - 6.days + 60, Time.now - 7.days + 60, Time.now - 5.days + 60 ] end it "has :#{record_mode} for the record mode" do expect(subject.record_mode).to eq(record_mode) end end context 'and the earliest recorded interaction was recorded more than 7 days ago' do let(:timestamps) do [ Time.now - 6.days - 60, Time.now - 7.days - 60, Time.now - 5.days - 60 ] end it "has :all for the record mode when there is an internet connection available" do allow(VCR::InternetConnection).to receive(:available?).and_return(true) expect(subject.record_mode).to eq(:all) end it "has :#{record_mode} for the record mode when there is no internet connection available" do allow(VCR::InternetConnection).to receive(:available?).and_return(false) expect(subject.record_mode).to eq(record_mode) end end end end end it 'does not load ignored interactions' do allow(VCR.request_ignorer).to receive(:ignore?) do |request| request.uri !~ /example\.com/ end VCR.configuration.cassette_library_dir = "#{VCR::SPEC_ROOT}/fixtures/cassette_spec" cassette = VCR::Cassette.new('with_localhost_requests', :record => record_mode) expect(cassette.send(:previously_recorded_interactions).map { |i| URI.parse(i.request.uri).host }).to eq(%w[example.com]) end it "loads the recorded interactions from the library yml file" do VCR.configuration.cassette_library_dir = "#{VCR::SPEC_ROOT}/fixtures/cassette_spec" cassette = VCR::Cassette.new('example', :record => record_mode) expect(cassette.send(:previously_recorded_interactions).size).to eq(3) i1, i2, i3 = *cassette.send(:previously_recorded_interactions) expect(i1.request.method).to eq(:get) expect(i1.request.uri).to eq('http://example.com/') expect(i1.response.body).to match(/You have reached this web page by typing.+example\.com/) expect(i2.request.method).to eq(:get) expect(i2.request.uri).to eq('http://example.com/foo') expect(i2.response.body).to match(/foo was not found on this server/) expect(i3.request.method).to eq(:get) expect(i3.request.uri).to eq('http://example.com/') expect(i3.response.body).to match(/Another example\.com response/) end [true, false].each do |value| it "instantiates the http_interactions with allow_playback_repeats = #{value} if given :allow_playback_repeats => #{value}" do VCR.configuration.cassette_library_dir = "#{VCR::SPEC_ROOT}/fixtures/cassette_spec" cassette = VCR::Cassette.new('example', :record => record_mode, :allow_playback_repeats => value) expect(cassette.http_interactions.allow_playback_repeats).to eq(value) end end it "instantiates the http_interactions with parent_list set to a null list if given :exclusive => true" do allow(VCR).to receive(:http_interactions).and_return(double) VCR.configuration.cassette_library_dir = "#{VCR::SPEC_ROOT}/fixtures/cassette_spec" cassette = VCR::Cassette.new('example', :record => record_mode, :exclusive => true) expect(cassette.http_interactions.parent_list).to be(VCR::Cassette::HTTPInteractionList::NullList) end it "instantiates the http_interactions with parent_list set to VCR.http_interactions if given :exclusive => false" do allow(VCR).to receive(:http_interactions).and_return(double) VCR.configuration.cassette_library_dir = "#{VCR::SPEC_ROOT}/fixtures/cassette_spec" cassette = VCR::Cassette.new('example', :record => record_mode, :exclusive => false) expect(cassette.http_interactions.parent_list).to be(VCR.http_interactions) end if stub_requests it 'invokes the before_playback hooks' do VCR.configuration.cassette_library_dir = "#{VCR::SPEC_ROOT}/fixtures/cassette_spec" expect(VCR.configuration).to receive(:invoke_hook).with( :before_playback, an_instance_of(VCR::HTTPInteraction::HookAware), an_instance_of(VCR::Cassette) ).exactly(3).times cassette = VCR::Cassette.new('example', :record => record_mode) expect(cassette.send(:previously_recorded_interactions).size).to eq(3) end it 'does not playback any interactions that are ignored in a before_playback hook' do VCR.configure do |c| c.before_playback { |i| i.ignore! if i.request.uri =~ /foo/ } end VCR.configuration.cassette_library_dir = "#{VCR::SPEC_ROOT}/fixtures/cassette_spec" cassette = VCR::Cassette.new('example', :record => record_mode) expect(cassette.send(:previously_recorded_interactions).size).to eq(2) end it 'instantiates the http_interactions with the loaded interactions and the request matchers' do VCR.configuration.cassette_library_dir = "#{VCR::SPEC_ROOT}/fixtures/cassette_spec" cassette = VCR::Cassette.new('example', :record => record_mode, :match_requests_on => [:body, :headers]) expect(cassette.http_interactions.interactions.size).to eq(3) expect(cassette.http_interactions.request_matchers).to eq([:body, :headers]) end else it 'instantiates the http_interactions with the no interactions and the request matchers' do VCR.configuration.cassette_library_dir = "#{VCR::SPEC_ROOT}/fixtures/cassette_spec" cassette = VCR::Cassette.new('example', :record => record_mode, :match_requests_on => [:body, :headers]) expect(cassette.http_interactions.interactions.size).to eq(0) expect(cassette.http_interactions.request_matchers).to eq([:body, :headers]) end end end end end describe ".originally_recorded_at" do it 'returns the earliest `recorded_at` timestamp' do i1 = http_interaction { |i| i.recorded_at = Time.now - 1000 } i2 = http_interaction { |i| i.recorded_at = Time.now - 10000 } i3 = http_interaction { |i| i.recorded_at = Time.now - 100 } stub_old_interactions([i1, i2, i3]) cassette = VCR::Cassette.new("example") expect(cassette.originally_recorded_at).to eq(i2.recorded_at) end it 'records nil for a cassette that has no prior recorded interactions' do stub_old_interactions([]) cassette = VCR::Cassette.new("example") expect(cassette.originally_recorded_at).to be_nil end end describe '#eject' do let(:custom_persister) { double("custom persister", :[] => nil) } context "when :allow_unused_http_interactions is set to false" do it 'asserts that there are no unused interactions' do cassette = VCR.insert_cassette("foo", :allow_unused_http_interactions => false) interaction_list = cassette.http_interactions expect(interaction_list).to respond_to(:assert_no_unused_interactions!).with(0).arguments expect(interaction_list).to receive(:assert_no_unused_interactions!) cassette.eject end it 'does not assert no unused interactions if there is an existing error' do cassette = VCR.insert_cassette("foo", :allow_unused_http_interactions => false) interaction_list = cassette.http_interactions allow(interaction_list).to receive(:assert_no_unused_interactions!) expect { begin raise "boom" ensure cassette.eject end }.to raise_error(/boom/) expect(interaction_list).not_to have_received(:assert_no_unused_interactions!) end it 'does not assert no unused interactions if :skip_no_unused_interactions_assertion is passed' do cassette = VCR.insert_cassette("foo", :allow_unused_http_interactions => false) interaction_list = cassette.http_interactions expect(interaction_list).not_to receive(:assert_no_unused_interactions!) cassette.eject(:skip_no_unused_interactions_assertion => true) end end it 'does not assert that there are no unused interactions if allow_unused_http_interactions is set to true' do cassette = VCR.insert_cassette("foo", :allow_unused_http_interactions => true) interaction_list = cassette.http_interactions expect(interaction_list).to respond_to(:assert_no_unused_interactions!) expect(interaction_list).not_to receive(:assert_no_unused_interactions!) cassette.eject end it 'stores the cassette content using the configured persister' do VCR.configuration.cassette_library_dir = nil VCR.cassette_persisters[:foo] = custom_persister cassette = VCR.insert_cassette("foo", :persist_with => :foo) cassette.record_http_interaction http_interaction expect(custom_persister).to receive(:[]=).with("foo.yml", /http_interactions/) cassette.eject end it "writes the serializable_hash to disk as yaml" do cassette = VCR::Cassette.new(:eject_test) cassette.record_http_interaction http_interaction # so it has one expect(cassette).to respond_to(:serializable_hash) allow(cassette).to receive(:serializable_hash).and_return({ "http_interactions" => [1, 3, 5] }) expect { cassette.eject }.to change { ::File.exist?(cassette.file) }.from(false).to(true) saved_stuff = YAML.load_file(cassette.file) expect(saved_stuff).to eq("http_interactions" => [1, 3, 5]) end it 'invokes the appropriately tagged before_record hooks' do interactions = [ http_interaction { |i| i.request.uri = 'http://foo.com/'; i.response.body = 'res 1' }, http_interaction { |i| i.request.uri = 'http://bar.com/'; i.response.body = 'res 2' } ] cassette = VCR::Cassette.new('example', :tag => :foo) allow(cassette).to receive(:new_recorded_interactions).and_return(interactions) allow(VCR.configuration).to receive(:invoke_hook).and_return([false]) interactions.each do |i| expect(VCR.configuration).to receive(:invoke_hook).with( :before_record, an_instance_of(VCR::HTTPInteraction::HookAware), cassette ).ordered end cassette.eject end it 'does not record interactions that have been ignored' do VCR.configure do |c| c.before_record { |i| i.ignore! if i.request.uri =~ /foo/ } end interaction_1 = http_interaction { |i| i.request.uri = 'http://foo.com/'; i.response.body = 'res 1' } interaction_2 = http_interaction { |i| i.request.uri = 'http://bar.com/'; i.response.body = 'res 2' } cassette = VCR::Cassette.new('test_cassette') allow(cassette).to receive(:new_recorded_interactions).and_return([interaction_1, interaction_2]) cassette.eject saved_recorded_interactions = ::YAML.load_file(cassette.file) expect(saved_recorded_interactions["http_interactions"]).to eq([interaction_2.to_hash]) end it 'does not write the cassette to disk if all interactions have been ignored' do VCR.configure do |c| c.before_record { |i| i.ignore! } end interaction_1 = http_interaction { |i| i.request.uri = 'http://foo.com/'; i.response.body = 'res 1' } cassette = VCR::Cassette.new('test_cassette') allow(cassette).to receive(:new_recorded_interactions).and_return([interaction_1]) cassette.eject expect(::File).not_to exist(cassette.file) end it "writes the recorded interactions to a subdirectory if the cassette name includes a directory" do recorded_interactions = [http_interaction { |i| i.response.body = "subdirectory response" }] cassette = VCR::Cassette.new('subdirectory/test_cassette') allow(cassette).to receive(:new_recorded_interactions).and_return(recorded_interactions) expect { cassette.eject }.to change { ::File.exist?(cassette.file) }.from(false).to(true) saved_recorded_interactions = YAML.load_file(cassette.file) expect(saved_recorded_interactions["http_interactions"]).to eq(recorded_interactions.map(&:to_hash)) end [:all, :none, :new_episodes].each do |record_mode| context "for a :record => :#{record_mode} cassette with previously recorded interactions" do subject { VCR::Cassette.new('example', :record => record_mode, :match_requests_on => [:uri]) } before(:each) do base_dir = "#{VCR::SPEC_ROOT}/fixtures/cassette_spec" FileUtils.cp(base_dir + "/example.yml", VCR.configuration.cassette_library_dir + "/example.yml") end it "does not re-write to disk the previously recorded interactions if there are no new ones" do yaml_file = subject.file expect(::File).not_to receive(:open).with(subject.file, 'w') expect { subject.eject }.to_not change { ::File.mtime(yaml_file) } end context 'when some new interactions have been recorded' do def interaction(response_body, request_attributes) http_interaction do |interaction| interaction.response.body = response_body request_attributes.each do |key, value| interaction.request.send("#{key}=", value) end end end let(:interaction_foo_1) { interaction("foo 1", :uri => 'http://foo.com/') } let(:interaction_foo_2) { interaction("foo 2", :uri => 'http://foo.com/') } let(:interaction_bar) { interaction("bar", :uri => 'http://bar.com/') } let(:saved_recorded_interactions) { YAML.load_file(subject.file)['http_interactions'].map { |h| VCR::HTTPInteraction.from_hash(h) } } let(:now) { Time.utc(2011, 6, 11, 12, 30) } before(:each) do allow(Time).to receive(:now).and_return(now) allow(subject).to receive(:previously_recorded_interactions).and_return([interaction_foo_1]) subject.record_http_interaction(interaction_foo_2) subject.record_http_interaction(interaction_bar) subject.eject end if record_mode == :all it 'replaces previously recorded interactions with new ones when the requests match' do expect(saved_recorded_interactions.first).to eq(interaction_foo_2) expect(saved_recorded_interactions).not_to include(interaction_foo_1) end it 'appends new recorded interactions that do not match existing ones' do expect(saved_recorded_interactions.last).to eq(interaction_bar) end else it 'appends new recorded interactions after existing ones' do expect(saved_recorded_interactions).to eq([interaction_foo_1, interaction_foo_2, interaction_bar]) end end end end end end end vcr-5.0.0/spec/lib/vcr/configuration_spec.rb000066400000000000000000000303071347305653000210170ustar00rootroot00000000000000require 'spec_helper' describe VCR::Configuration do describe '#cassette_library_dir=' do let(:tmp_dir) { VCR::SPEC_ROOT + '/../tmp/cassette_library_dir/new_dir' } after(:each) { FileUtils.rm_rf tmp_dir } it 'creates the directory if it does not exist' do expect { subject.cassette_library_dir = tmp_dir }.to change { File.exist?(tmp_dir) }.from(false).to(true) end it 'does not raise an error if given nil' do expect { subject.cassette_library_dir = nil }.to_not raise_error end it 'resolves the given directory to an absolute path, so VCR continues to work even if the current directory changes' do relative_dir = 'tmp/cassette_library_dir/new_dir' subject.cassette_library_dir = relative_dir absolute_dir = File.join(VCR::SPEC_ROOT.sub(/\/spec\z/, ''), relative_dir) expect(subject.cassette_library_dir).to eq(absolute_dir) end end describe '#default_cassette_options' do it 'has a hash with some defaults' do expect(subject.default_cassette_options).to eq({ :match_requests_on => VCR::RequestMatcherRegistry::DEFAULT_MATCHERS, :allow_unused_http_interactions => true, :record => :once, :serialize_with => :yaml, :persist_with => :file_system }) end it "returns #{VCR::RequestMatcherRegistry::DEFAULT_MATCHERS.inspect} for :match_requests_on when other defaults have been set" do subject.default_cassette_options = { :record => :none } expect(subject.default_cassette_options).to include(:match_requests_on => VCR::RequestMatcherRegistry::DEFAULT_MATCHERS) end it "returns :once for :record when other defaults have been set" do subject.default_cassette_options = { :erb => :true } expect(subject.default_cassette_options).to include(:record => :once) end it "allows defaults to be overriden" do subject.default_cassette_options = { :record => :all } expect(subject.default_cassette_options).to include(:record => :all) end it "allows other keys to be set" do subject.default_cassette_options = { :re_record_interval => 10 } expect(subject.default_cassette_options).to include(:re_record_interval => 10) end end describe '#register_request_matcher' do it 'registers the given request matcher' do expect { VCR.request_matchers[:custom] }.to raise_error(VCR::UnregisteredMatcherError) matcher_run = false subject.register_request_matcher(:custom) { |r1, r2| matcher_run = true } VCR.request_matchers[:custom].matches?(:r1, :r2) expect(matcher_run).to be true end end describe '#hook_into' do it 'requires the named library hook' do expect(subject).to receive(:require).with("vcr/library_hooks/webmock") expect(subject).to receive(:require).with("vcr/library_hooks/excon") subject.hook_into :webmock, :excon end it 'raises an error for unsupported stubbing libraries' do expect { subject.hook_into :unsupported_library }.to raise_error(ArgumentError, /unsupported_library is not a supported VCR HTTP library hook/i) end it 'invokes the after_library_hooks_loaded hooks' do called = false subject.after_library_hooks_loaded { called = true } subject.hook_into :webmock expect(called).to be true end end describe '#ignore_hosts' do it 'delegates to the current request_ignorer instance' do expect(VCR.request_ignorer).to receive(:ignore_hosts).with('example.com', 'example.net') subject.ignore_hosts 'example.com', 'example.net' end end describe '#unignore_hosts' do it 'delegates to the current request_ignorer instance' do expect(VCR.request_ignorer).to receive(:unignore_hosts).with('example.com', 'example.net') subject.unignore_hosts 'example.com', 'example.net' end end describe '#ignore_localhost=' do it 'delegates to the current request_ignorer instance' do expect(VCR.request_ignorer).to receive(:ignore_localhost=).with(true) subject.ignore_localhost = true end end describe '#ignore_request' do let(:uri){ URI('http://foo.com') } it 'registers the given block with the request ignorer' do block_called = false subject.ignore_request { |r| block_called = true } VCR.request_ignorer.ignore?(double(:parsed_uri => uri)) expect(block_called).to be true end end describe '#allow_http_connections_when_no_cassette=' do [true, false].each do |val| it "sets the allow_http_connections_when_no_cassette to #{val} when set to #{val}" do subject.allow_http_connections_when_no_cassette = val expect(subject.allow_http_connections_when_no_cassette?).to eq(val) end end end describe "request/configuration interactions", :with_monkey_patches => :webmock do specify 'the request on the yielded interaction is not typed even though the request given to before_http_request is' do before_record_req = before_request_req = nil VCR.configure do |c| c.before_http_request { |r| before_request_req = r } c.before_record { |i| before_record_req = i.request } end VCR.use_cassette("example") do ::Net::HTTP.get_response(URI("http://localhost:#{VCR::SinatraApp.port}/foo")) end expect(before_record_req).not_to respond_to(:type) expect(before_request_req).to respond_to(:type) end unless (RUBY_VERSION =~ /^1\.8/ || RUBY_INTERPRETER == :jruby) specify 'the filter_sensitive_data option works even when it modifies the URL in a way that makes it an invalid URI' do VCR.configure do |c| c.filter_sensitive_data('') { 'localhost' } end 2.times do VCR.use_cassette("example") do ::Net::HTTP.get_response(URI("http://localhost:#{VCR::SinatraApp.port}/foo")) end end end end [:before_record, :before_playback].each do |hook_type| describe "##{hook_type}" do it 'sets up a tag filter' do called = false VCR.configuration.send(hook_type, :my_tag) { called = true } VCR.configuration.invoke_hook(hook_type, double, double(:tags => [])) expect(called).to be false VCR.configuration.invoke_hook(hook_type, double, double(:tags => [:my_tag])) expect(called).to be true end end end %w[ filter_sensitive_data define_cassette_placeholder ].each do |method| describe "##{method}" do let(:interaction) { double('interaction').as_null_object } before(:each) { allow(interaction).to receive(:filter!) } it 'adds a before_record hook that replaces the string returned by the block with the given string' do subject.send(method, 'foo', &lambda { 'bar' }) expect(interaction).to receive(:filter!).with('bar', 'foo') subject.invoke_hook(:before_record, interaction, double.as_null_object) end it 'adds a before_playback hook that replaces the given string with the string returned by the block' do subject.send(method, 'foo', &lambda { 'bar' }) expect(interaction).to receive(:filter!).with('foo', 'bar') subject.invoke_hook(:before_playback, interaction, double.as_null_object) end it 'tags the before_record hook when given a tag' do expect(subject).to receive(:before_record).with(:my_tag) subject.send(method, 'foo', :my_tag) { 'bar' } end it 'tags the before_playback hook when given a tag' do expect(subject).to receive(:before_playback).with(:my_tag) subject.send(method, 'foo', :my_tag) { 'bar' } end it 'yields the interaction to the block for the before_record hook' do yielded_interaction = nil subject.send(method, 'foo', &lambda { |i| yielded_interaction = i; 'bar' }) subject.invoke_hook(:before_record, interaction, double.as_null_object) expect(yielded_interaction).to equal(interaction) end it 'yields the interaction to the block for the before_playback hook' do yielded_interaction = nil subject.send(method, 'foo', &lambda { |i| yielded_interaction = i; 'bar' }) subject.invoke_hook(:before_playback, interaction, double.as_null_object) expect(yielded_interaction).to equal(interaction) end end end describe "#after_http_request" do let(:raw_request) { VCR::Request.new } let(:response) { VCR::Response.new } def request(type) VCR::Request::Typed.new(raw_request, type) end it 'handles symbol request predicate filters properly' do yielded = false subject.after_http_request(:stubbed_by_vcr?) { |req| yielded = true } subject.invoke_hook(:after_http_request, request(:stubbed_by_vcr), response) expect(yielded).to be true yielded = false subject.invoke_hook(:after_http_request, request(:ignored), response) expect(yielded).to be false end end describe "#cassette_serializers" do let(:custom_serializer) { double } it 'allows a custom serializer to be registered' do expect { subject.cassette_serializers[:custom] }.to raise_error(ArgumentError) subject.cassette_serializers[:custom] = custom_serializer expect(subject.cassette_serializers[:custom]).to be(custom_serializer) end end describe "#cassette_persisters" do let(:custom_persister) { double } it 'allows a custom persister to be registered' do expect { subject.cassette_persisters[:custom] }.to raise_error(ArgumentError) subject.cassette_persisters[:custom] = custom_persister expect(subject.cassette_persisters[:custom]).to be(custom_persister) end end describe "#uri_parser=" do let(:custom_parser) { double } it 'allows a custom uri parser to be set' do subject.uri_parser = custom_parser expect(subject.uri_parser).to eq(custom_parser) end it "uses Ruby's standard library `URI` as a default" do expect(subject.uri_parser).to eq(URI) end end describe "#preserve_exact_body_bytes_for?" do def message_for(body) double(:body => body) end context "default hook" do it "returns false when there is no current cassette" do expect(subject.preserve_exact_body_bytes_for?(message_for "string")).to be false end it "returns false when the current cassette has been created without the :preserve_exact_body_bytes option" do VCR.insert_cassette('foo') expect(subject.preserve_exact_body_bytes_for?(message_for "string")).to be false end it 'returns true when the current cassette has been created with the :preserve_exact_body_bytes option' do VCR.insert_cassette('foo', :preserve_exact_body_bytes => true) expect(subject.preserve_exact_body_bytes_for?(message_for "string")).to be true end end it "returns true when the configured block returns true" do subject.preserve_exact_body_bytes { |msg| msg.body == "a" } expect(subject.preserve_exact_body_bytes_for?(message_for "a")).to be true expect(subject.preserve_exact_body_bytes_for?(message_for "b")).to be false end it "returns true when any of the registered blocks returns true" do called_hooks = [] subject.preserve_exact_body_bytes { called_hooks << :hook_1; false } subject.preserve_exact_body_bytes { called_hooks << :hook_2; true } expect(subject.preserve_exact_body_bytes_for?(message_for "a")).to be true expect(called_hooks).to eq([:hook_1, :hook_2]) end it "invokes the configured hook with the http message and the current cassette" do VCR.use_cassette('example') do |cassette| expect(cassette).to be_a(VCR::Cassette) message = double(:message) yielded_objects = nil subject.preserve_exact_body_bytes { |a, b| yielded_objects = [a, b] } subject.preserve_exact_body_bytes_for?(message) expect(yielded_objects).to eq([message, cassette]) end end end describe "#configure_rspec_metadata!" do it "only configures the underlying metadata once, no matter how many times it is called" do expect(VCR::RSpec::Metadata).to receive(:configure!).once VCR.configure do |c| c.configure_rspec_metadata! end VCR.configure do |c| c.configure_rspec_metadata! end end end end vcr-5.0.0/spec/lib/vcr/deprecations_spec.rb000066400000000000000000000042771347305653000206370ustar00rootroot00000000000000require 'spec_helper' describe VCR, 'deprecations', :disable_warnings do describe ".config" do it 'delegates to VCR.configure' do expect(VCR).to receive(:configure) VCR.config { } end it 'yields the configuration object' do config_object = nil VCR.config { |c| config_object = c } expect(config_object).to be(VCR.configuration) end it 'prints a deprecation warning' do expect(VCR).to receive(:warn).with(/VCR.config.*deprecated/i) VCR.config { } end end describe "Config" do it 'returns the same object referenced by VCR.configuration' do expect(VCR::Config).to be(VCR.configuration) end it 'prints a deprecation warning' do expect(VCR).to receive(:warn).with(/VCR::Config.*deprecated/i) VCR::Config end it 'preserves the normal undefined constant behavior' do expect { VCR::SomeUndefinedConstant }.to raise_error(NameError) end end describe "Cassette::MissingERBVariableError" do it 'returns VCR::Errors::MissingERBVariableError' do expect(VCR::Cassette::MissingERBVariableError).to be(VCR::Errors::MissingERBVariableError) end it 'prints a deprecation warning' do expect(VCR::Cassette).to receive(:warn).with(/VCR::Cassette::MissingERBVariableError.*deprecated/i) VCR::Cassette::MissingERBVariableError end it 'preserves the normal undefined constant behavior' do expect { VCR::Cassette::SomeUndefinedConstant }.to raise_error(NameError) end end describe "VCR.configure { |c| c.stub_with ... }" do it 'delegates to #hook_into' do expect(VCR.configuration).to receive(:hook_into).with(:webmock, :excon) VCR.configure { |c| c.stub_with :webmock, :excon } end it 'prints a deprecation warning' do expect(VCR.configuration).to receive(:warn).with(/stub_with.*deprecated/i) VCR.configure { |c| c.stub_with :webmock, :excon } end end describe "VCR::Middleware::Faraday" do it 'prints a deprecation warning when passed a block' do expect(Kernel).to receive(:warn).with(/Passing a block .* is deprecated/) VCR::Middleware::Faraday.new(double) { } end end end vcr-5.0.0/spec/lib/vcr/errors_spec.rb000066400000000000000000000205241347305653000174640ustar00rootroot00000000000000require 'spec_helper' module VCR module Errors describe UnhandledHTTPRequestError do def message_for(request_values = {}) described_class.new(request_with request_values).message end alias message message_for def request_with(options) VCR::Request.new(*options.values_at(*VCR::Request.members)) end it 'identifies the request by method and URI' do expect(message_for(:method => :post, :uri => 'http://foo.com/')).to include( 'POST http://foo.com/' ) end context 'when there is no cassette' do it 'identifies the request by its body when the default_cassette_options include the body in the match_requests_on option' do VCR.configuration.default_cassette_options[:match_requests_on] = [:body] expect(message_for(:body => 'param=val1')).to include( "Body: param=val1" ) end it 'identifies the request by its headers when the default_cassette_options include the headers in the match_requests_on option' do VCR.configuration.default_cassette_options[:match_requests_on] = [:headers] expect(message_for(:headers => { "Content-Type" => "application/json", "X-Custom" => ["123", "ab\"c"]})).to include( 'Content-Type: "application/json"', 'X-Custom: "123"', 'X-Custom: "ab\"c"' ) end it 'mentions that there is no cassette' do expect(message).to include('There is currently no cassette in use.') end it 'mentions that the request can be recorded by inserting a cassette' do expect(message).to match(/record this request and play it back.*VCR.use_cassette/m) end it 'mentions the allow_http_connections_when_no_cassette option' do expect(message).to include('allow_http_connections_when_no_cassette') end it 'mentions that the request can be ignored' do expect(message).to include('set an `ignore_request` callback') end it 'does not double-insert the asterisks for the bullet points' do expect(message).not_to match(/\s+\*\s+\*/) end it 'mentions the debug logging configuration option' do expect(message).to include('debug_logger') end end context 'when there are cassettes' do it 'identifies the request by its body when the match_requests_on option includes the body' do VCR.use_cassette('example', :match_requests_on => [:body]) do expect(message_for(:body => 'param=val1')).to include( "Body: param=val1" ) end end it 'identifies the request by its headers when the match_requests_on option includes the headers' do VCR.use_cassette('example', :match_requests_on => [:headers]) do expect(message_for(:headers => { "Content-Type" => "application/json", "X-Custom" => ["123", "ab\"c"]})).to include( 'Content-Type: "application/json"', 'X-Custom: "123"', 'X-Custom: "ab\"c"' ) end end it 'does not identify the request by its body when the cassette match_requests_on option does not include the body but the default_cassette_options do' do VCR.configuration.default_cassette_options[:match_requests_on] = [:body] VCR.use_cassette('example', :match_requests_on => [:uri]) do expect(message_for(:body => 'param=val1')).to_not match(/body/i) end end it 'mentions the details about the single cassette when there is one cassette' do VCR.use_cassette('example') do expect(message).to match(/VCR is currently using the following cassette:.+example.yml/m) end end it 'mentions the details about all cassettes when there are a few cassettes' do VCR.use_cassette('example') do VCR.use_cassette('sample') do expect(message).to match(/VCR are currently using the following cassettes:.+sample.yml.+example.yml/m) end end end it 'mentions that :new_episodes can be used to record the request' do VCR.use_cassette('example') do expect(message).to include('use the :new_episodes record mode') end end it 'mentions that :once does not allow a cassette to be re-recorded' do VCR.use_cassette('example', :record => :once) do expect(message).to include('(:once) does not allow new requests to be recorded') end end it 'mentions that :none does not allow any recording' do VCR.use_cassette('example', :record => :none) do expect(message).to include('(:none) does not allow requests to be recorded') end end it 'does not mention the :once or :none record modes if using the :new_episodes record mode' do VCR.use_cassette('example', :record => :new_episodes) do expect(message).not_to include(':once', ':none') end end it 'does not mention the :once or :none record modes if using the :new_episodes record mode at least in one cassette' do VCR.use_cassette('example', :record => :new_episodes) do VCR.use_cassette('sample') do expect(message).not_to include('current record mode (:once)', 'current record mode (:none)') end end end it 'mentions :allow_playback_repeats if the cassette has a used matching interaction' do VCR.use_cassette('example') do |cassette| expect(cassette.http_interactions).to respond_to(:has_used_interaction_matching?) allow(cassette.http_interactions).to receive(:has_used_interaction_matching?).and_return(true) expect(message).to include('allow_playback_repeats') end end it 'does not mention :allow_playback_repeats if the cassette does not have a used matching interaction' do VCR.use_cassette('example') do |cassette| expect(cassette.http_interactions).to respond_to(:has_used_interaction_matching?) allow(cassette.http_interactions).to receive(:has_used_interaction_matching?).and_return(false) expect(message).not_to include('allow_playback_repeats') end end it 'does not mention using a different :match_requests_on option when there are no remaining unused interactions' do VCR.use_cassette('example') do |cassette| expect(cassette.http_interactions).to respond_to(:remaining_unused_interaction_count) allow(cassette.http_interactions).to receive(:remaining_unused_interaction_count).and_return(0) expect(message).not_to include('match_requests_on cassette option') end end it 'mentions using a different :match_requests_on option when there are some remaining unused interactions' do VCR.use_cassette('example') do |cassette| expect(cassette.http_interactions).to respond_to(:remaining_unused_interaction_count) allow(cassette.http_interactions).to receive(:remaining_unused_interaction_count).and_return(1) expect(message).to include('match_requests_on cassette option') end end it 'uses the singular (HTTP interaction) when there is only 1 left' do VCR.use_cassette('example') do |cassette| expect(cassette.http_interactions).to respond_to(:remaining_unused_interaction_count) allow(cassette.http_interactions).to receive(:remaining_unused_interaction_count).and_return(1) expect(message).to include('1 HTTP interaction ') end end it 'uses the plural (HTTP interactions) when there is more than 1 left' do VCR.use_cassette('example') do |cassette| expect(cassette.http_interactions).to respond_to(:remaining_unused_interaction_count) allow(cassette.http_interactions).to receive(:remaining_unused_interaction_count).and_return(2) expect(message).to include('2 HTTP interactions ') end end it 'mentions the debug logging configuration option' do VCR.use_cassette('example', :record => :new_episodes) do expect(message).to include('debug_logger') end end end end end end vcr-5.0.0/spec/lib/vcr/library_hooks/000077500000000000000000000000001347305653000174555ustar00rootroot00000000000000vcr-5.0.0/spec/lib/vcr/library_hooks/excon_spec.rb000066400000000000000000000065461347305653000221430ustar00rootroot00000000000000require 'spec_helper' require 'support/shared_example_groups/excon' describe "Excon hook", :with_monkey_patches => :excon do after(:each) do ::Excon.stubs.clear ::Excon.defaults[:mock] = false end def directly_stub_request(method, url, response_body) ::Excon.defaults[:mock] = true ::Excon.stub({ :method => method, :url => url }, { :body => response_body }) end it_behaves_like 'a hook into an HTTP library', :excon, 'excon', :status_message_not_exposed context "when the query is specified as a hash option" do let(:excon) { ::Excon.new("http://localhost:#{VCR::SinatraApp.port}/search") } it 'properly records and plays back the response' do allow(VCR).to receive(:real_http_connections_allowed?).and_return(true) recorded, played_back = [1, 2].map do VCR.use_cassette('excon_query', :record => :once) do excon.request(:method => :get, :query => { :q => 'Tolkien' }).body end end expect(recorded).to eq(played_back) expect(recorded).to eq('query: Tolkien') end end context "when Excon's expects and idempotent middlewares cause errors to be raised" do let(:excon) { ::Excon.new("http://localhost:#{VCR::SinatraApp.port}/404_not_200") } def make_request VCR.use_cassette('with_errors', :record => :once) do excon.request(:method => :get, :expects => [200], :idempotent => true).body end end it 'records and plays back properly' do expect { make_request }.to raise_error(Excon::Errors::NotFound) expect { make_request }.to raise_error(Excon::Errors::NotFound) end end include_examples "Excon streaming" context 'when Excon raises an error due to an unexpected response status' do before(:each) do allow(VCR).to receive(:real_http_connections_allowed?).and_return(true) end it 'still records properly' do expect(VCR).to receive(:record_http_interaction) do |interaction| expect(interaction.response.status.code).to eq(404) expect(interaction.response.body).to eq('404 not 200') end expect { Excon.get("http://localhost:#{VCR::SinatraApp.port}/404_not_200", :expects => 200) }.to raise_error(Excon::Errors::Error) end def error_raised_by yield rescue => e return e else raise "No error was raised" end it 'raises the same error class as excon itself raises' do real_error, stubbed_error = 2.times.map do error_raised_by do VCR.use_cassette('excon_error', :record => :once) do Excon.get("http://localhost:#{VCR::SinatraApp.port}/not_found", :expects => 200) end end end expect(stubbed_error.class).to be(real_error.class) end it_behaves_like "request hooks", :excon, :recordable do undef make_request def make_request(disabled = false) expect { Excon.get(request_url, :expects => 404) }.to raise_error(Excon::Errors::Error) end end end describe "VCR.configuration.after_library_hooks_loaded hook" do it 'disables the webmock excon adapter so it does not conflict with our typhoeus hook' do expect(::WebMock::HttpLibAdapters::ExconAdapter).to respond_to(:disable!) expect(::WebMock::HttpLibAdapters::ExconAdapter).to receive(:disable!) $excon_after_loaded_hook.conditionally_invoke end end end vcr-5.0.0/spec/lib/vcr/library_hooks/faraday_spec.rb000066400000000000000000000036721347305653000224330ustar00rootroot00000000000000require 'spec_helper' require 'vcr/library_hooks/faraday' describe "Faraday hook" do it 'inserts the VCR middleware just before the adapter' do conn = Faraday.new(:url => 'http://sushi.com') do |builder| builder.request :url_encoded builder.response :logger builder.adapter :net_http end conn.builder.lock! expect(conn.builder.handlers.last(2).map(&:klass)).to eq([ VCR::Middleware::Faraday, Faraday::Adapter::NetHttp ]) end it 'handles the case where no adapter is declared' do conn = Faraday.new conn.builder.lock! expect(conn.builder.handlers.last(2).map(&:klass)).to eq([ VCR::Middleware::Faraday, Faraday::Adapter::NetHttp ]) end it 'does nothing if the VCR middleware has already been included' do conn = Faraday.new(:url => 'http://sushi.com') do |builder| builder.use VCR::Middleware::Faraday builder.use Faraday::Response::Logger builder.use Faraday::Adapter::NetHttp end conn.builder.lock! expect(conn.builder.handlers.map(&:klass)).to eq([ VCR::Middleware::Faraday, Faraday::Response::Logger, Faraday::Adapter::NetHttp ]) end it 'prints a warning if the faraday connection stack contains a middleware after the HTTP adapter' do conn = Faraday.new(:url => 'http://sushi.com') do |builder| builder.use Faraday::Adapter::NetHttp builder.use Faraday::Response::Logger end expect(conn.builder).to receive(:warn).with(/Faraday::Response::Logger/) conn.builder.lock! end it 'gracefully handles the case where there is no explicit HTTP adapter' do conn = Faraday.new(:url => 'http://sushi.com') do |builder| builder.request :url_encoded builder.response :logger end conn.builder.lock! expect(conn.builder.handlers.map(&:klass)).to eq([ Faraday::Request::UrlEncoded, Faraday::Response::Logger, VCR::Middleware::Faraday ]) end end vcr-5.0.0/spec/lib/vcr/library_hooks/typhoeus_0.4_spec.rb000066400000000000000000000024011347305653000232520ustar00rootroot00000000000000require 'spec_helper' describe "Typhoeus 0.4 hook", :with_monkey_patches => :typhoeus_0_4 do after(:each) do ::Typhoeus::Hydra.clear_stubs end def disable_real_connections ::Typhoeus::Hydra.allow_net_connect = false ::Typhoeus::Hydra::NetConnectNotAllowedError end def enable_real_connections ::Typhoeus::Hydra.allow_net_connect = true end def directly_stub_request(method, url, response_body) response = ::Typhoeus::Response.new(:code => 200, :body => response_body) ::Typhoeus::Hydra.stub(method, url).and_return(response) end it_behaves_like 'a hook into an HTTP library', :typhoeus, 'typhoeus 0.4' describe "VCR.configuration.after_library_hooks_loaded hook" do it 'disables the webmock typhoeus adapter so it does not conflict with our typhoeus hook' do expect(::WebMock::HttpLibAdapters::TyphoeusAdapter).to receive(:disable!) $typhoeus_after_loaded_hook.conditionally_invoke end it "warns about Typhoeus 0.4 deprecation" do expect(::Kernel).to receive(:warn).with("WARNING: VCR's Typhoeus 0.4 integration is deprecated and will be removed in VCR 3.0.") $typhoeus_0_4_after_loaded_hook.conditionally_invoke end end end if RUBY_INTERPRETER == :mri && ::Typhoeus::VERSION.to_f < 0.5 vcr-5.0.0/spec/lib/vcr/library_hooks/typhoeus_spec.rb000066400000000000000000000134721347305653000227030ustar00rootroot00000000000000require 'spec_helper' describe "Typhoeus hook", :with_monkey_patches => :typhoeus, :if => (RUBY_INTERPRETER == :mri) do after(:each) do ::Typhoeus::Expectation.clear end def disable_real_connections ::Typhoeus::Config.block_connection = true ::Typhoeus::Errors::NoStub end def enable_real_connections ::Typhoeus::Config.block_connection = false end def directly_stub_request(method, url, response_body) response = ::Typhoeus::Response.new(:code => 200, :body => response_body) ::Typhoeus.stub(url, :method => method).and_return(response) end it_behaves_like 'a hook into an HTTP library', :typhoeus, 'typhoeus' describe "VCR.configuration.after_library_hooks_loaded hook" do it 'disables the webmock typhoeus adapter so it does not conflict with our typhoeus hook' do expect(::WebMock::HttpLibAdapters::TyphoeusAdapter).to receive(:disable!) $typhoeus_after_loaded_hook.conditionally_invoke end end context 'when there are nested hydra queues' do def make_requests VCR.use_cassette("nested") do response_1 = response_2 = nil hydra = Typhoeus::Hydra.new request = Typhoeus::Request.new("http://localhost:#{VCR::SinatraApp.port}/") request.on_success do |r1| response_1 = r1 nested = Typhoeus::Request.new("http://localhost:#{VCR::SinatraApp.port}/foo") nested.on_success { |r2| response_2 = r2 } hydra.queue(nested) end hydra.queue(request) hydra.run return body_for(response_1), body_for(response_2) end end def body_for(response) return :no_response if response.nil? response.body end it 'records and plays back properly' do recorded = make_requests played_back = make_requests expect(played_back).to eq(recorded) end end context "when used with a typhoeus-based faraday connection" do let(:base_url) { "http://localhost:#{VCR::SinatraApp.port}" } let(:conn) do Faraday.new(:url => base_url) do |faraday| faraday.adapter :typhoeus end end def get_response # Ensure faraday hook doesn't handle the request. VCR.library_hooks.exclusively_enabled(:typhoeus) do VCR.use_cassette("faraday") do conn.get("/") end end end it 'records and replays headers correctly' do recorded = get_response played_back = get_response expect(played_back.headers).to eq(recorded.headers) end end context 'when a request is made with a hash for the POST body' do def make_request VCR.use_cassette("hash_body") do Typhoeus::Request.post( "http://localhost:#{VCR::SinatraApp.port}/return-request-body", :body => { :foo => "17" } ) end end it 'records and replays correctly' do recorded = make_request played_back = make_request expect(recorded.body).to eq("foo=17") expect(played_back.body).to eq(recorded.body) end end context 'when using on_body callback' do def make_request VCR.use_cassette('no_body') do request = Typhoeus::Request.new("http://localhost:#{VCR::SinatraApp.port}/localhost_test") request.on_headers { on_headers_counter.increment } request.on_body { on_body_counter.increment } request.run end end let(:on_headers_counter) { double(:increment => nil) } let(:on_body_counter) { double(:increment => nil) } it 'records and replays correctly' do expect(on_headers_counter).to receive(:increment).exactly(2).times expect(on_body_counter).to receive(:increment).exactly(2).times recorded = make_request played_back = make_request expect(recorded.body).to eq('Localhost response') expect(played_back.body).to eq(recorded.body) end end context 'when using on_body callback returning :abort' do def make_request VCR.use_cassette('no_body') do request = Typhoeus::Request.new("http://localhost:#{VCR::SinatraApp.port}/localhost_test") request.on_body { next :abort } request.run end end it 'records and replays correctly' do recorded = make_request played_back = make_request expect(recorded.body).to eq('Localhost response') expect(played_back.body).to eq(recorded.body) end end context '#effective_url' do ResponseValues = Struct.new(:status, :body, :effective_url) def url_for(path) "http://localhost:#{VCR::SinatraApp.port}#{path}" end def make_single_request(path, options = {}) VCR.use_cassette('single') do |cassette| response = Typhoeus::Request.new(url_for(path), options).run yield cassette if block_given? ResponseValues.new( response.code, response.body, response.effective_url ) end end it 'records and plays back properly' do recorded = make_single_request('/') played_back = make_single_request('/') expect(recorded.effective_url).to eq(url_for('/')) expect(played_back).to eq(recorded) end it 'falls back to the request url when it was not recorded (e.g. on VCR <= 2.5.0)' do make_single_request('/') do |cassette| cassette.new_recorded_interactions.each { |i| i.response.adapter_metadata.clear } end played_back = make_single_request('/') expect(played_back.effective_url).to eq(url_for('/')) end context "when following redirects" do it 'records and plays back properly' do recorded = make_single_request('/redirect-to-root', :followlocation => true) played_back = make_single_request('/redirect-to-root', :followlocation => true) expect(recorded.effective_url).to eq(url_for('/')) expect(played_back).to eq(recorded) end end end end vcr-5.0.0/spec/lib/vcr/library_hooks/webmock_spec.rb000066400000000000000000000072551347305653000224540ustar00rootroot00000000000000require 'spec_helper' require 'support/shared_example_groups/excon' describe "WebMock hook", :with_monkey_patches => :webmock do after(:each) do ::WebMock.reset! end def disable_real_connections(options = {}) ::WebMock.disable_net_connect!(options) ::WebMock::NetConnectNotAllowedError end def enable_real_connections ::WebMock.allow_net_connect! end def directly_stub_request(method, url, response_body) ::WebMock.stub_request(method, url).to_return(:body => response_body) end describe "our WebMock.after_request hook" do let(:webmock_request) { ::WebMock::RequestSignature.new(:get, "http://foo.com/", :body => "", :headers => {}) } let(:webmock_response) { ::WebMock::Response.new(:body => 'OK', :status => [200, '']) } def run_after_request_callback ::WebMock::CallbackRegistry.invoke_callbacks( { :real_request => true }, webmock_request, webmock_response) end it 'removes the @__typed_vcr_request instance variable so as not to pollute the webmock object' do request = VCR::Request::Typed.new(VCR::Request, :ignored?) webmock_request.instance_variable_set(:@__typed_vcr_request, request) run_after_request_callback expect(webmock_request.instance_variables.map(&:to_sym)).not_to include(:@__typed_vcr_request) end context "when there'ss a bug and the request does not have the @__typed_vcr_request in the after_request callbacks" do let(:warner) { VCR::LibraryHooks::WebMock } before { allow(warner).to receive(:warn) } it 'records the HTTP interaction properly' do expect(VCR).to receive(:record_http_interaction) do |i| expect(i.request.uri).to eq("http://foo.com/") expect(i.response.body).to eq("OK") end run_after_request_callback end it 'invokes the after_http_request hook with an :unknown request' do request = nil VCR.configuration.after_http_request do |req, res| request = req end run_after_request_callback expect(request.uri).to eq("http://foo.com/") expect(request.type).to eq(:unknown) end it 'prints a warning' do expect(warner).to receive(:warn).at_least(:once).with(/bug.*after_request/) run_after_request_callback end end end http_libs = %w[net/http patron httpclient em-http-request curb typhoeus excon] http_libs.each do |lib| other = [] other << :status_message_not_exposed if lib == 'excon' it_behaves_like 'a hook into an HTTP library', :webmock, lib, *other do if lib == 'net/http' def normalize_request_headers(headers) headers.merge(DEFAULT_REQUEST_HEADERS) end end end http_lib_unsupported = (RUBY_INTERPRETER != :mri && lib =~ /(typhoeus|curb|patron|em-http)/) adapter_module = HTTP_LIBRARY_ADAPTERS.fetch(lib) describe "using #{adapter_module.http_library_name}", :unless => http_lib_unsupported do include adapter_module let!(:request_url) { "http://localhost:#{VCR::SinatraApp.port}/foo" } context 'when real connections are disabled and VCR is turned off' do it 'can allow connections to localhost' do VCR.turn_off! disable_real_connections(:allow_localhost => true) expect { make_http_request(:get, request_url) }.to_not raise_error end it 'can allow connections to matching urls' do VCR.turn_off! disable_real_connections(:allow => /foo/) expect { make_http_request(:get, request_url) }.to_not raise_error end end end end it_behaves_like "Excon streaming" end vcr-5.0.0/spec/lib/vcr/library_hooks_spec.rb000066400000000000000000000030731347305653000210170ustar00rootroot00000000000000require 'vcr/library_hooks' module VCR describe LibraryHooks do describe '#disabled?' do it 'returns false by default for any argument given' do expect(subject.disabled?(:foo)).to be false expect(subject.disabled?(:bar)).to be false end context 'when a library hook is exclusively enabled' do it 'returns false for the exclusively enabled hook' do faraday_disabled = nil subject.exclusively_enabled :faraday do faraday_disabled = subject.disabled?(:faraday) end expect(faraday_disabled).to eq(false) end it 'returns true for every other argument given' do foo_disabled = bar_disabled = nil subject.exclusively_enabled :faraday do foo_disabled = subject.disabled?(:foo) bar_disabled = subject.disabled?(:bar) end expect(foo_disabled).to be true expect(bar_disabled).to be true end end end describe '#exclusively_enabled' do it 'restores all hook to being enabled when the block completes' do subject.exclusively_enabled(:faraday) { } expect(subject.disabled?(:foo)).to be false expect(subject.disabled?(:faraday)).to be false end it 'restores all hooks to being enabled when the block completes, even if there is an error' do subject.exclusively_enabled(:faraday) { raise "boom" } rescue expect(subject.disabled?(:foo)).to be false expect(subject.disabled?(:faraday)).to be false end end end end vcr-5.0.0/spec/lib/vcr/middleware/000077500000000000000000000000001347305653000167235ustar00rootroot00000000000000vcr-5.0.0/spec/lib/vcr/middleware/faraday_spec.rb000066400000000000000000000130641347305653000216750ustar00rootroot00000000000000require 'spec_helper' require 'vcr/library_hooks/faraday' describe VCR::Middleware::Faraday do http_libs = %w[ typhoeus net_http patron ] http_libs.each do |lib| flags = [ :does_not_support_rotating_responses ] if lib == 'typhoeus' flags << :status_message_not_exposed end it_behaves_like 'a hook into an HTTP library', :faraday, "faraday (w/ #{lib})", *flags end context 'when performing a multipart upload' do let(:connection) do ::Faraday.new("http://localhost:#{VCR::SinatraApp.port}/") do |b| b.request :multipart end end def self.test_recording it 'records the request body correctly' do payload = { :file => Faraday::UploadIO.new(__FILE__, 'text/plain') } expect(VCR).to receive(:record_http_interaction) do |i| expect(i.request.headers['Content-Type'].first).to include("multipart") expect(i.request.body).to include(File.read(__FILE__)) end VCR.use_cassette("upload") do connection.post '/files', payload end end end context 'when the net_http adapter is used' do before { connection.builder.adapter :net_http } test_recording end context 'when no adapter is used' do test_recording end end context 'when extending the response body with an extension module' do let(:connection) { ::Faraday.new("http://localhost:#{VCR::SinatraApp.port}/") } def process_response(response) response.body.extend Module.new { attr_accessor :_response } response.body._response = response end it 'does not record the body extensions to the cassette' do 3.times do |i| VCR.use_cassette("hack", :record => :new_episodes) do response = connection.get("/foo") process_response(response) # Do something different after the first time to # ensure new interactions are added to an existing # cassette. if i > 1 response = connection.get("/") process_response(response) end end end contents = VCR::Cassette.new("hack").send(:raw_cassette_bytes) expect(contents).not_to include("ruby/object:Faraday::Response") end end context 'when making parallel requests' do include VCRStubHelpers let(:connection) { ::Faraday.new { |b| b.adapter :typhoeus } } let(:request_url) { "http://localhost:#{VCR::SinatraApp.port}/" } it 'works correctly with multiple parallel requests' do recorded, played_back = [1, 2].map do responses = [] VCR.use_cassette("multiple_parallel") do connection.in_parallel do responses << connection.get(request_url) responses << connection.get(request_url) end end responses.map(&:body) end # there should be no blanks expect(recorded.select { |r| r.to_s == '' }).to eq([]) expect(played_back).to eq(recorded) end shared_examples_for "exclusive library hook" do def make_request connection.in_parallel { connection.get(request_url) } end it 'makes the faraday middleware exclusively enabled for the duration of the request' do expect(VCR.library_hooks).not_to be_disabled(:webmock) hook_called = false VCR.configuration.after_http_request do hook_called = true expect(VCR.library_hooks).to be_disabled(:webmock) end make_request expect(VCR.library_hooks).not_to be_disabled(:webmock) expect(hook_called).to be true end end context 'for an ignored request' do before(:each) { VCR.configuration.ignore_request { true } } it_behaves_like "exclusive library hook" end context 'for a stubbed request' do it_behaves_like "exclusive library hook" do before(:each) do stub_requests([http_interaction(request_url)], [:method, :uri]) end end end context "when another adapter is exclusive" do it 'still makes requests properly' do response = VCR.library_hooks.exclusively_enabled(:typhoeus) do Faraday.get("http://localhost:#{VCR::SinatraApp.port}/") end expect(response.body).to eq("GET to root") end end context 'for a recorded request' do let!(:inserted_cassette) { VCR.insert_cassette('new_cassette') } before(:each) { expect(VCR).to receive(:record_http_interaction) } it_behaves_like "exclusive library hook" end context 'for a disallowed request' do it_behaves_like "exclusive library hook" do undef make_request def make_request expect { connection.in_parallel { connection.get(request_url) } }.to raise_error(VCR::Errors::UnhandledHTTPRequestError) end end end it_behaves_like "request hooks", :faraday, :recordable do let!(:inserted_cassette) { VCR.insert_cassette('new_cassette') } undef make_request def make_request(disabled = false) response = nil connection.in_parallel do response = connection.get(request_url) end response end it 'can be used to eject a cassette after the request is recorded' do VCR.configuration.after_http_request { |request| VCR.eject_cassette } expect(VCR).to receive(:record_http_interaction) do |interaction| expect(VCR.current_cassette).to be(inserted_cassette) end make_request expect(VCR.current_cassette).to be_nil end end end if defined?(::Typhoeus) end vcr-5.0.0/spec/lib/vcr/middleware/rack_spec.rb000066400000000000000000000066401347305653000212100ustar00rootroot00000000000000require 'spec_helper' require 'vcr/middleware/rack' module VCR module Middleware describe CassetteArguments do describe '#name' do it 'initially returns nil' do expect(subject.name).to be_nil end it 'stores the given value, returning it when no arg is given' do subject.name :value1 expect(subject.name).to eq(:value1) subject.name :value2 expect(subject.name).to eq(:value2) end end describe '#options' do it 'initially returns an empty hash' do expect(subject.options).to eq({}) end it 'merges the given hash options, returning them when no arg is given' do subject.options :record => :new_episodes expect(subject.options).to eq({ :record => :new_episodes }) subject.options :erb => true expect(subject.options).to eq({ :record => :new_episodes, :erb => true }) end end end describe Rack do describe '.new' do it 'raises an error if no cassette arguments block is provided' do expect { described_class.new(lambda { |env| }) }.to raise_error(ArgumentError) end end describe '#call' do let(:env_hash) { { :env => :hash } } it 'calls the provided rack app and returns its response' do rack_app = double expect(rack_app).to receive(:call).with(env_hash).and_return(:response) instance = described_class.new(rack_app) { |c| c.name 'cassette_name' } expect(instance.call(env_hash)).to eq(:response) end it 'uses a cassette when the rack app is called' do expect(VCR.current_cassette).to be_nil rack_app = lambda { |env| expect(VCR.current_cassette).not_to be_nil } instance = described_class.new(rack_app) { |c| c.name 'cassette_name' } instance.call({}) expect(VCR.current_cassette).to be_nil end it 'sets the cassette name based on the provided block' do rack_app = lambda do |env| expect(VCR.current_cassette.name).to eq('rack_cassette') end instance = described_class.new(rack_app) { |c| c.name 'rack_cassette' } instance.call({}) end it 'sets the cassette options based on the provided block' do rack_app = lambda do |env| expect(VCR.current_cassette.erb).to eq({ :foo => :bar }) end instance = described_class.new(rack_app, &lambda do |c| c.name 'c' c.options :erb => { :foo => :bar } end) instance.call({}) end it 'yields the rack env to the provided block when the block accepts 2 arguments' do instance = described_class.new(lambda { |env| }, &lambda do |c, env| expect(env).to eq(env_hash) c.name 'c' end) instance.call(env_hash) end end let(:threaded_app) do lambda do |env| sleep 0.15 expect(VCR.send(:cassettes).size).to eq(1) [200, {}, ['OK']] end end it 'is thread safe' do stack = described_class.new(threaded_app) do |cassette| cassette.name 'c' end thread = Thread.new { stack.call({}) } stack.call({}) thread.join expect(VCR.current_cassette).to be_nil end end end end vcr-5.0.0/spec/lib/vcr/request_ignorer_spec.rb000066400000000000000000000062661347305653000213740ustar00rootroot00000000000000require 'vcr/structs' require 'vcr/request_ignorer' module VCR describe RequestIgnorer do def request(uri) VCR::Request.new.tap { |r| r.uri = uri } end shared_examples_for "#ignore?" do |url, expected_value| it "returns #{expected_value} if given a request with a url like #{url}" do expect(subject.ignore?(request(url))).to eq(expected_value) end end context 'when example.com and example.net are ignored' do before(:each) { subject.ignore_hosts 'example.com', 'example.net' } it_behaves_like "#ignore?", "http://www.example.com/foo", false it_behaves_like "#ignore?", "http://example.com/foo", true it_behaves_like "#ignore?", "http://example.net:890/foo", true it_behaves_like "#ignore?", "http://some-other-domain.com/", false end context 'when example.com is unignored' do before(:each) do subject.instance_variable_set(:@ignored_hosts, Set['example.com']) subject.unignore_hosts 'example.com' end it_behaves_like "#ignore?", "http://example.com/foo", false end context 'when two of three example hosts are unignored' do before(:each) do subject.instance_variable_set(:@ignored_hosts, Set['example.com', 'example.net', 'example.org']) subject.unignore_hosts 'example.com', 'example.net' end it_behaves_like "#ignore?", "http://example.com/foo", false it_behaves_like "#ignore?", "http://example.net:890/foo", false it_behaves_like "#ignore?", "https://example.org:890/foo", true end context 'when not ignored host is unignored' do it 'no errors should be raised' do expect { subject.unignore_hosts 'example.com' }.not_to raise_error end end context 'when ignore_localhost is set to true' do before(:each) { subject.ignore_localhost = true } it_behaves_like "#ignore?", "http://some-host.com/foo", false RequestIgnorer::LOCALHOST_ALIASES.each do |host| it_behaves_like "#ignore?", "http://#{host}/foo", true end end context 'when ignore_localhost is not set' do it_behaves_like "#ignore?", "http://some-host.com/foo", false RequestIgnorer::LOCALHOST_ALIASES.each do |host| it_behaves_like "#ignore?", "http://#{host}/foo", false end end context 'when ignore_localhost is set to false after being set to true' do before(:each) do subject.ignore_localhost = true subject.ignore_localhost = false end it_behaves_like "#ignore?", "http://some-host.com/foo", false RequestIgnorer::LOCALHOST_ALIASES.each do |host| it_behaves_like "#ignore?", "http://#{host}/foo", false end end context 'when a custom ignore_request hook has been set' do before(:each) do subject.ignore_request do |request| URI(request.uri).port == 5 end end it 'ignores requests for which the block returns true' do expect(subject.ignore?(request('http://foo.com:5/bar'))).to be true end it 'does not ignore requests for which the block returns false' do expect(subject.ignore?(request('http://foo.com:6/bar'))).to be false end end end end vcr-5.0.0/spec/lib/vcr/request_matcher_registry_spec.rb000066400000000000000000000270411347305653000232740ustar00rootroot00000000000000require 'vcr/request_matcher_registry' require 'vcr/structs' require 'support/limited_uri' require 'cgi' require 'support/configuration_stubbing' module VCR describe RequestMatcherRegistry do include_context "configuration stubbing" before do allow(config).to receive(:uri_parser) { LimitedURI } allow(config).to receive(:query_parser) { CGI.method(:parse) } end def request_with(values) VCR::Request.new.tap do |request| values.each do |name, value| request.send("#{name}=", value) end end end describe "#register" do it 'registers a request matcher block that can be used later' do matcher_called = false subject.register(:my_matcher) { |*a| matcher_called = true } subject[:my_matcher].matches?(double, double) expect(matcher_called).to be true end context 'when there is already a matcher for the given name' do before(:each) do subject.register(:foo) { |*a| false } allow(subject).to receive :warn end it 'overrides the existing matcher' do subject.register(:foo) { |*a| true } expect(subject[:foo].matches?(double, double)).to be true end it 'warns that there is a name collision' do expect(subject).to receive(:warn).with( /WARNING: There is already a VCR request matcher registered for :foo\. Overriding it/ ) subject.register(:foo) { |*a| true } end end end describe "#[]" do it 'returns a previously registered matcher' do matcher = lambda { } subject.register(:my_matcher, &matcher) expect(subject[:my_matcher]).to eq(RequestMatcherRegistry::Matcher.new(matcher)) end it 'raises an ArgumentError when no matcher has been registered for the given name' do expect { subject[:some_unregistered_matcher] }.to raise_error(VCR::Errors::UnregisteredMatcherError) end it 'returns an object that calls the named block when #matches? is called on it' do subject.register(:foo) { |r1, r2| r1 == 5 || r2 == 10 } expect(subject[:foo].matches?(5, 0)).to be true expect(subject[:foo].matches?(0, 10)).to be true expect(subject[:foo].matches?(7, 7)).to be false end it 'returns an object that calls the given callable when #matches? is called on it' do block_called = false subject[lambda { |r1, r2| block_called = true }].matches?(5, 0) expect(block_called).to be true end end [:uri_without_param, :uri_without_params].each do |meth| describe "##{meth}" do it 'returns a matcher that can be registered for later use' do matcher = subject.send(meth, :foo) subject.register(:uri_without_foo, &matcher) matches = subject[:uri_without_foo].matches?( request_with(:uri => 'http://example.com/search?foo=123'), request_with(:uri => 'http://example.com/search?foo=123') ) expect(matches).to be true end it 'matches two requests with URIs that are identical' do matches = subject[subject.send(meth, :foo)].matches?( request_with(:uri => 'http://example.com/search?foo=123'), request_with(:uri => 'http://example.com/search?foo=123') ) expect(matches).to be true end it 'does not match two requests with different path parts' do matches = subject[subject.send(meth, :foo)].matches?( request_with(:uri => 'http://example.com/search?foo=123'), request_with(:uri => 'http://example.com/find?foo=123') ) expect(matches).to be false end it 'ignores the given query parameters when it is at the start' do matches = subject[subject.send(meth, :foo)].matches?( request_with(:uri => 'http://example.com/search?foo=123&bar=r'), request_with(:uri => 'http://example.com/search?foo=124&bar=r') ) expect(matches).to be true end it 'ignores the given query parameters when it is at the end' do matches = subject[subject.send(meth, :bar)].matches?( request_with(:uri => 'http://example.com/search?foo=124&bar=r'), request_with(:uri => 'http://example.com/search?foo=124&bar=q') ) expect(matches).to be true end it 'still takes into account other query params' do matches = subject[subject.send(meth, :bar)].matches?( request_with(:uri => 'http://example.com/search?foo=123&bar=r'), request_with(:uri => 'http://example.com/search?foo=124&bar=q') ) expect(matches).to be false end it 'handles multiple query params of the same name' do matches = subject[subject.send(meth, :tag)].matches?( request_with(:uri => 'http://example.com/search?foo=124&tag[]=a&tag[]=b'), request_with(:uri => 'http://example.com/search?foo=124&tag[]=d&tag[]=e') ) expect(matches).to be true end it 'can ignore multiple named parameters' do matches = subject[subject.send(meth, :foo, :bar)].matches?( request_with(:uri => 'http://example.com/search?foo=123&bar=r&baz=9'), request_with(:uri => 'http://example.com/search?foo=124&baz=9&bar=q') ) expect(matches).to be true end it 'matches two requests with URIs that have no params' do matches = subject[subject.send(meth, :foo, :bar)].matches?( request_with(:uri => 'http://example.com/search'), request_with(:uri => 'http://example.com/search') ) expect(matches).to be true end it 'does not match two requests with URIs that have no params but different paths' do matches = subject[subject.send(meth, :foo, :bar)].matches?( request_with(:uri => 'http://example.com/foo'), request_with(:uri => 'http://example.com/bar') ) expect(matches).to be false end it 'matches a second request when all parameters are filtered' do matches = subject[subject.send(meth, :q, :oq)].matches?( request_with(:uri => 'http://example.com/search'), request_with(:uri => 'http://example.com/search?q=vcr&oq=vcr') ) expect(matches).to be true end end end describe "built-ins" do describe ":method" do it 'matches when it is the same' do matches = subject[:method].matches?( request_with(:method => :get), request_with(:method => :get) ) expect(matches).to be true end it 'does not match when it is not the same' do matches = subject[:method].matches?( request_with(:method => :get), request_with(:method => :post) ) expect(matches).to be false end end describe ":uri" do it 'matches when it is exactly the same' do matches = subject[:uri].matches?( request_with(:uri => 'http://foo.com/bar?baz=7'), request_with(:uri => 'http://foo.com/bar?baz=7') ) expect(matches).to be true end it 'does not match when it is different' do matches = subject[:uri].matches?( request_with(:uri => 'http://foo1.com/bar?baz=7'), request_with(:uri => 'http://foo2.com/bar?baz=7') ) expect(matches).to be false end end describe ":host" do it 'matches when it is the same' do matches = subject[:host].matches?( request_with(:uri => 'http://foo.com/bar'), request_with(:uri => 'http://foo.com/car') ) expect(matches).to be true end it 'does not match when it is not the same' do matches = subject[:host].matches?( request_with(:uri => 'http://foo.com/bar'), request_with(:uri => 'http://goo.com/bar') ) expect(matches).to be false end end describe ":path" do it 'matches when it is the same' do matches = subject[:path].matches?( request_with(:uri => 'http://foo.com/bar?a=8'), request_with(:uri => 'http://goo.com/bar?a=9') ) expect(matches).to be true end it 'does not match when it is not the same' do matches = subject[:path].matches?( request_with(:uri => 'http://foo.com/bar?a=8'), request_with(:uri => 'http://foo.com/car?a=8') ) expect(matches).to be false end end describe ":body" do it 'matches when it is the same' do matches = subject[:body].matches?( request_with(:body => 'foo'), request_with(:body => 'foo') ) expect(matches).to be true end it 'does not match when it is not the same' do matches = subject[:body].matches?( request_with(:body => 'foo'), request_with(:body => 'bar') ) expect(matches).to be false end end describe ":body_as_json" do it 'matches when the body json is reordered' do matches = subject[:body_as_json].matches?( request_with(:body => '{ "a": "1", "b": "2" }'), request_with(:body => '{ "b": "2", "a": "1" }') ) expect(matches).to be true end it 'does not match when json is not the same' do matches = subject[:body_as_json].matches?( request_with(:body => '{ "a": "1", "b": "2" }'), request_with(:body => '{ "a": "1", "b": "1" }') ) expect(matches).to be false end end describe ":headers" do it 'matches when it is the same' do matches = subject[:headers].matches?( request_with(:headers => { 'a' => 1, 'b' => 2 }), request_with(:headers => { 'b' => 2, 'a' => 1 }) ) expect(matches).to be true end it 'does not match when it is not the same' do matches = subject[:headers].matches?( request_with(:headers => { 'a' => 3, 'b' => 2 }), request_with(:headers => { 'b' => 2, 'a' => 1 }) ) expect(matches).to be false end end describe ":query" do it 'matches when it is identical' do matches = subject[:query].matches?( request_with(:uri => 'http://foo.com/bar?a=8'), request_with(:uri => 'http://goo.com/car?a=8') ) expect(matches).to be true end it 'matches when empty' do matches = subject[:query].matches?( request_with(:uri => 'http://foo.com/bar'), request_with(:uri => 'http://goo.com/car') ) expect(matches).to be true end it 'matches when parameters are reordered' do matches = subject[:query].matches?( request_with(:uri => 'http://foo.com/bar?a=8&b=9'), request_with(:uri => 'http://goo.com/car?b=9&a=8') ) expect(matches).to be true end it 'does not match when it is not the same' do matches = subject[:query].matches?( request_with(:uri => 'http://foo.com/bar?a=8'), request_with(:uri => 'http://goo.com/car?b=8') ) expect(matches).to be false end end end end end vcr-5.0.0/spec/lib/vcr/structs_spec.rb000066400000000000000000000615101347305653000176570ustar00rootroot00000000000000# encoding: UTF-8 require 'support/ruby_interpreter' require 'yaml' require 'vcr/structs' require 'vcr/errors' require 'zlib' require 'stringio' require 'support/limited_uri' require 'support/configuration_stubbing' shared_examples_for "a header normalizer" do let(:instance) do with_headers('Some_Header' => 'value1', 'aNother' => ['a', 'b'], 'third' => [], 'fourth' => nil) end it 'ensures header keys are serialized to yaml as raw strings' do key = 'my-key' key.instance_variable_set(:@foo, 7) instance = with_headers(key => ['value1']) expect(YAML.dump(instance.headers)).to eq(YAML.dump('my-key' => ['value1'])) end it 'ensures header values are serialized to yaml as raw strings' do value = 'my-value' value.instance_variable_set(:@foo, 7) instance = with_headers('my-key' => [value]) expect(YAML.dump(instance.headers)).to eq(YAML.dump('my-key' => ['my-value'])) end it 'handles nested arrays' do accept_encoding = [["gzip", "1.0"], ["deflate", "1.0"], ["sdch", "1.0"]] instance = with_headers('accept-encoding' => accept_encoding) expect(instance.headers['accept-encoding']).to eq(accept_encoding) end it 'handles nested arrays with floats' do accept_encoding = [["gzip", 1.0], ["deflate", 1.0], ["sdch", 1.0]] instance = with_headers('accept-encoding' => accept_encoding) expect(instance.headers['accept-encoding']).to eq(accept_encoding) end end shared_examples_for "a body normalizer" do it "ensures the body is serialized to yaml as a raw string" do body = "My String" body.instance_variable_set(:@foo, 7) expect(YAML.dump(instance(body).body)).to eq(YAML.dump("My String")) end it 'converts nil to a blank string' do expect(instance(nil).body).to eq("") end it 'raises an error if given another type of object as the body' do expect { instance(:a => "hash") }.to raise_error(ArgumentError) end end module VCR describe HTTPInteraction do include_context "configuration stubbing" before { allow(config).to receive(:uri_parser) { LimitedURI } } if ''.respond_to?(:encoding) def body_hash(key, value) { key => value, 'encoding' => 'UTF-8' } end else def body_hash(key, value) { key => value } end end describe "#recorded_at" do let(:now) { Time.now } it 'is initialized to the current time' do allow(Time).to receive(:now).and_return(now) expect(VCR::HTTPInteraction.new.recorded_at).to eq(now) end end let(:status) { ResponseStatus.new(200, "OK") } let(:response) { Response.new(status, { "foo" => ["bar"] }, "res body", "1.1") } let(:request) { Request.new(:get, "http://foo.com/", "req body", { "bar" => ["foo"] }) } let(:recorded_at) { Time.utc(2011, 5, 4, 12, 30) } let(:interaction) { HTTPInteraction.new(request, response, recorded_at) } describe ".from_hash" do let(:hash) do { 'request' => { 'method' => 'get', 'uri' => 'http://foo.com/', 'body' => body_hash('string', 'req body'), 'headers' => { "bar" => ["foo"] } }, 'response' => { 'status' => { 'code' => 200, 'message' => 'OK' }, 'headers' => { "foo" => ["bar"] }, 'body' => body_hash('string', 'res body'), 'http_version' => '1.1' }, 'recorded_at' => "Wed, 04 May 2011 12:30:00 GMT" } end it 'constructs an HTTP interaction from the given hash' do expect(HTTPInteraction.from_hash(hash)).to eq(interaction) end it 'initializes the recorded_at timestamp from the hash' do expect(HTTPInteraction.from_hash(hash).recorded_at).to eq(recorded_at) end it 'initializes the response adapter_metadata from the hash if it is included' do hash['response']['adapter_metadata'] = { 'foo' => 12 } interaction = HTTPInteraction.from_hash(hash) expect(interaction.response.adapter_metadata).to eq("foo" => 12) end it 'works when the response adapter_metadata is missing' do expect(hash['response'].keys).not_to include('adapter_metadata') interaction = HTTPInteraction.from_hash(hash) expect(interaction.response.adapter_metadata).to eq({}) end it 'uses a blank request when the hash lacks one' do hash.delete('request') i = HTTPInteraction.from_hash(hash) expect(i.request).to eq(Request.new) end it 'uses a blank response when the hash lacks one' do hash.delete('response') i = HTTPInteraction.from_hash(hash) expect(i.response).to eq(Response.new(ResponseStatus.new)) end it 'decodes the base64 body string' do hash['request']['body'] = body_hash('base64_string', Base64.encode64('req body')) hash['response']['body'] = body_hash('base64_string', Base64.encode64('res body')) i = HTTPInteraction.from_hash(hash) expect(i.request.body).to eq('req body') expect(i.response.body).to eq('res body') end if ''.respond_to?(:encoding) it 'force encodes the decoded base64 string as the original encoding' do string = "café" string.force_encoding("US-ASCII") expect(string).not_to be_valid_encoding hash['request']['body'] = { 'base64_string' => Base64.encode64(string.dup), 'encoding' => 'US-ASCII' } hash['response']['body'] = { 'base64_string' => Base64.encode64(string.dup), 'encoding' => 'US-ASCII' } i = HTTPInteraction.from_hash(hash) expect(i.request.body.encoding.name).to eq("US-ASCII") expect(i.response.body.encoding.name).to eq("US-ASCII") expect(i.request.body.bytes.to_a).to eq(string.bytes.to_a) expect(i.response.body.bytes.to_a).to eq(string.bytes.to_a) expect(i.request.body).not_to be_valid_encoding expect(i.response.body).not_to be_valid_encoding end it 'does not attempt to force encode the decoded base64 string when there is no encoding given (i.e. if the cassette was recorded on ruby 1.8)' do hash['request']['body'] = { 'base64_string' => Base64.encode64('foo') } i = HTTPInteraction.from_hash(hash) expect(i.request.body).to eq('foo') expect(i.request.body.encoding.name).to eq("ASCII-8BIT") end it 'tries to encode strings to the original encoding' do hash['request']['body'] = { 'string' => "abc", 'encoding' => 'ISO-8859-1' } hash['response']['body'] = { 'string' => "abc", 'encoding' => 'ISO-8859-1' } i = HTTPInteraction.from_hash(hash) expect(i.request.body).to eq("abc") expect(i.response.body).to eq("abc") expect(i.request.body.encoding.name).to eq("ISO-8859-1") expect(i.response.body.encoding.name).to eq("ISO-8859-1") end it 'does not attempt to encode the string when there is no encoding given (i.e. if the cassette was recorded on ruby 1.8)' do string = 'foo' string.force_encoding("ISO-8859-1") hash['request']['body'] = { 'string' => string } i = HTTPInteraction.from_hash(hash) expect(i.request.body).to eq('foo') expect(i.request.body.encoding.name).to eq("ISO-8859-1") end it 'force encodes to ASCII-8BIT (since it just means "no encoding" or binary)' do string = "\u00f6" string.encode("UTF-8") expect(string).to be_valid_encoding hash['request']['body'] = { 'string' => string, 'encoding' => 'ASCII-8BIT' } expect(Request).not_to receive(:warn) i = HTTPInteraction.from_hash(hash) expect(i.request.body).to eq(string) expect(i.request.body.bytes.to_a).to eq(string.bytes.to_a) expect(i.request.body.encoding.name).to eq("ASCII-8BIT") end context 'when the string cannot be encoded as the original encoding' do def verify_encoding_error expect { "\xFAbc".encode("ISO-8859-1") }.to raise_error(EncodingError) end before do allow(Request).to receive(:warn) allow(Response).to receive(:warn) hash['request']['body'] = { 'string' => "\xFAbc", 'encoding' => 'ISO-8859-1' } hash['response']['body'] = { 'string' => "\xFAbc", 'encoding' => 'ISO-8859-1' } verify_encoding_error end it 'does not force the encoding' do i = HTTPInteraction.from_hash(hash) expect(i.request.body).to eq("\xFAbc") expect(i.response.body).to eq("\xFAbc") expect(i.request.body.encoding.name).not_to eq("ISO-8859-1") expect(i.response.body.encoding.name).not_to eq("ISO-8859-1") end it 'prints a warning and informs users of the :preserve_exact_body_bytes option' do expect(Request).to receive(:warn).with(/ISO-8859-1.*preserve_exact_body_bytes/) expect(Response).to receive(:warn).with(/ISO-8859-1.*preserve_exact_body_bytes/) HTTPInteraction.from_hash(hash) end end end end describe "#to_hash" do include_context "configuration stubbing" before(:each) do allow(config).to receive(:preserve_exact_body_bytes_for?).and_return(false) allow(config).to receive(:uri_parser).and_return(URI) end let(:hash) { interaction.to_hash } it 'returns a nested hash containing all of the pertinent details' do expect(hash.keys).to match_array %w[ request response recorded_at ] expect(hash['recorded_at']).to eq(interaction.recorded_at.httpdate) expect(hash['request']).to eq({ 'method' => 'get', 'uri' => 'http://foo.com/', 'body' => body_hash('string', 'req body'), 'headers' => { "bar" => ["foo"] } }) expect(hash['response']).to eq({ 'status' => { 'code' => 200, 'message' => 'OK' }, 'headers' => { "foo" => ["bar"] }, 'body' => body_hash('string', 'res body'), 'http_version' => '1.1' }) end it 'includes the response adapter metadata when it is not empty' do interaction.response.adapter_metadata['foo'] = 17 expect(hash['response']['adapter_metadata']).to eq('foo' => 17) end it 'does not include the response adapter metadata when it is empty' do expect(interaction.response.adapter_metadata).to eq({}) expect(hash['response'].keys).not_to include('adapter_metadata') end context "when the body is extended with a module and some state" do it 'serializes to YAML w/o the extra state' do interaction.request.body.extend Module.new { attr_accessor :foo } interaction.response.body.extend Module.new { attr_accessor :foo } interaction.request.body.foo = 98765 interaction.response.body.foo = 98765 expect(YAML.dump(interaction.to_hash)).not_to include("98765") end end it 'encodes the body as base64 when the configuration is so set' do allow(config).to receive(:preserve_exact_body_bytes_for?).and_return(true) expect(hash['request']['body']).to eq(body_hash('base64_string', Base64.encode64('req body'))) expect(hash['response']['body']).to eq(body_hash('base64_string', Base64.encode64('res body'))) end it "sets the string's original encoding", :if => ''.respond_to?(:encoding) do interaction.request.body.force_encoding('ISO-8859-10') interaction.response.body.force_encoding('ASCII-8BIT') expect(hash['request']['body']['encoding']).to eq('ISO-8859-10') expect(hash['response']['body']['encoding']).to eq('ASCII-8BIT') end def assert_yielded_keys(hash, *keys) yielded_keys = [] hash.each { |k, v| yielded_keys << k } expect(yielded_keys).to eq(keys) end it 'yields the entries in the expected order so the hash can be serialized in that order' do assert_yielded_keys hash, 'request', 'response', 'recorded_at' assert_yielded_keys hash['request'], 'method', 'uri', 'body', 'headers' assert_yielded_keys hash['response'], 'status', 'headers', 'body', 'http_version' assert_yielded_keys hash['response']['status'], 'code', 'message' end it 'yields `adapter_metadata` if it has any data' do interaction.response.adapter_metadata['foo'] = 17 assert_yielded_keys hash['response'], 'status', 'headers', 'body', 'http_version', 'adapter_metadata' end end describe "#parsed_uri" do before :each do allow(uri_parser).to receive(:parse).and_return(uri) allow(config).to receive(:uri_parser).and_return(uri_parser) end let(:uri_parser){ double('parser') } let(:uri){ double('uri').as_null_object } it "parses the uri using the current uri_parser" do expect(uri_parser).to receive(:parse).with(request.uri) request.parsed_uri end it "returns the parsed uri" do expect(request.parsed_uri).to eq uri end end end describe HTTPInteraction::HookAware do include_context "configuration stubbing" before do allow(config).to receive(:uri_parser) { LimitedURI } end let(:response_status) { VCR::ResponseStatus.new(200, "OK foo") } let(:body) { "The body foo this is (foo-Foo)" } let(:headers) do { 'x-http-foo' => ['bar23', '23foo'], 'x-http-bar' => ['foo23', '18'] } end let(:response) do VCR::Response.new( response_status, headers.dup, body.dup, '1.1' ) end let(:request) do VCR::Request.new( :get, 'http://example-foo.com:80/foo/', body.dup, headers.dup ) end let(:interaction) { VCR::HTTPInteraction.new(request, response) } subject { HTTPInteraction::HookAware.new(interaction) } describe '#ignored?' do it 'returns false by default' do should_not be_ignored end it 'returns true when #ignore! has been called' do subject.ignore! should be_ignored end end describe '#filter!' do let(:filtered) { subject.filter!('foo', 'AAA') } it 'does nothing when given a blank argument' do expect { subject.filter!(nil, 'AAA') subject.filter!('foo', nil) subject.filter!("", 'AAA') subject.filter!('foo', "") }.not_to change { interaction } end [:request, :response].each do |part| it "replaces the sensitive text in the #{part} header keys and values" do expect(filtered.send(part).headers).to eq({ 'x-http-AAA' => ['bar23', '23AAA'], 'x-http-bar' => ['AAA23', '18'] }) end it "replaces the sensitive text in the #{part} body" do expect(filtered.send(part).body).to eq("The body AAA this is (AAA-Foo)") end end it 'replaces the sensitive text in the response status' do expect(filtered.response.status.message).to eq('OK AAA') end it 'replaces sensitive text in the request URI' do expect(filtered.request.uri).to eq('http://example-AAA.com/AAA/') end it 'handles numbers (such as the port) properly' do request.uri = "http://foo.com:9000/bar" subject.filter!(9000, "") expect(request.uri).to eq("http://foo.com:/bar") end end end describe Request::Typed do [:uri, :method, :headers, :body].each do |method| it "delegates ##{method} to the request" do request = double(method => "delegated value") expect(Request::Typed.new(request, :type).send(method)).to eq("delegated value") end end describe "#type" do it 'returns the initialized type' do expect(Request::Typed.new(double, :ignored).type).to be(:ignored) end end valid_types = [:ignored, :stubbed_by_vcr, :externally_stubbed, :recordable, :unhandled] valid_types.each do |type| describe "##{type}?" do it "returns true if the type is set to :#{type}" do expect(Request::Typed.new(double, type).send("#{type}?")).to be true end it "returns false if the type is set to :other" do expect(Request::Typed.new(double, :other).send("#{type}?")).to be false end end end describe "#real?" do real_types = [:ignored, :recordable] real_types.each do |type| it "returns true if the type is set to :#{type}" do expect(Request::Typed.new(double, type)).to be_real end end (valid_types - real_types).each do |type| it "returns false if the type is set to :#{type}" do expect(Request::Typed.new(double, type)).not_to be_real end end end describe "#stubbed?" do stubbed_types = [:externally_stubbed, :stubbed_by_vcr] stubbed_types.each do |type| it "returns true if the type is set to :#{type}" do expect(Request::Typed.new(double, type)).to be_stubbed end end (valid_types - stubbed_types).each do |type| it "returns false if the type is set to :#{type}" do expect(Request::Typed.new(double, type)).not_to be_stubbed end end end end describe Request do include_context "configuration stubbing" before do allow(config).to receive(:uri_parser) { LimitedURI } end describe '#method' do subject { VCR::Request.new(:get) } context 'when given no arguments' do it 'returns the HTTP method' do expect(subject.method).to eq(:get) end end context 'when given an argument' do it 'returns the method object for the named method' do m = subject.method(:class) expect(m).to be_a(Method) expect(m.call).to eq(described_class) end end it 'gets normalized to a lowercase symbol' do expect(VCR::Request.new("GET").method).to eq(:get) expect(VCR::Request.new(:GET).method).to eq(:get) expect(VCR::Request.new(:get).method).to eq(:get) expect(VCR::Request.new("get").method).to eq(:get) end end describe "#uri" do def uri_for(uri) VCR::Request.new(:get, uri).uri end it 'removes the default http port' do expect(uri_for("http://foo.com:80/bar")).to eq("http://foo.com/bar") end it 'removes the default https port' do expect(uri_for("https://foo.com:443/bar")).to eq("https://foo.com/bar") end it 'does not remove a non-standard http port' do expect(uri_for("http://foo.com:81/bar")).to eq("http://foo.com:81/bar") end it 'does not remove a non-standard https port' do expect(uri_for("https://foo.com:442/bar")).to eq("https://foo.com:442/bar") end end describe Request::FiberAware do subject { Request::FiberAware.new(Request.new) } it 'adds a #proceed method that yields in a fiber' do fiber = Fiber.new do |request| request.proceed :done end expect(fiber.resume(subject)).to be_nil expect(fiber.resume).to eq(:done) end it 'can be cast to a proc' do expect(Fiber).to receive(:yield) lambda(&subject).call end end if RUBY_VERSION > '1.9' it_behaves_like 'a header normalizer' do def with_headers(headers) described_class.new(:get, 'http://example.com/', nil, headers) end end it_behaves_like 'a body normalizer' do def instance(body) described_class.new(:get, 'http://example.com/', body, {}) end end end describe Response do it_behaves_like 'a header normalizer' do def with_headers(headers) described_class.new(:status, headers, nil, '1.1') end end it_behaves_like 'a body normalizer' do def instance(body) described_class.new(:status, {}, body, '1.1') end end describe "#adapter_metadata" do it 'returns the hash given as the last #initialize argument' do response = Response.new( ResponseStatus.new(200, "OK"), {}, "the body", "1.1", { "meta" => "value" } ) expect(response.adapter_metadata).to eq("meta" => "value") end it 'returns a blank hash when nil is passed to #initialize' do response = Response.new( ResponseStatus.new(200, "OK"), {}, "the body", "1.1", nil ) expect(response.adapter_metadata).to eq({}) end end describe '#update_content_length_header' do %w[ content-length Content-Length ].each do |header| context "for the #{header} header" do define_method :instance do |body, content_length| headers = { 'content-type' => 'text' } headers.merge!(header => content_length) if content_length described_class.new(VCR::ResponseStatus.new, headers, body) end it 'does nothing when the response lacks a content_length header' do inst = instance('the body', nil) expect { inst.update_content_length_header }.not_to change { inst.headers[header] } end it 'sets the content_length header to the response body length when the header is present' do inst = instance('the body', '3') expect { inst.update_content_length_header }.to change { inst.headers[header] }.from(['3']).to(['8']) end it 'sets the content_length header to 0 if the response body is nil' do inst = instance(nil, '3') expect { inst.update_content_length_header }.to change { inst.headers[header] }.from(['3']).to(['0']) end it 'sets the header according to RFC 2616 based on the number of bytes (not the number of characters)' do inst = instance('aؼ', '2') # the second char is a double byte char expect { inst.update_content_length_header }.to change { inst.headers[header] }.from(['2']).to(['3']) end end end end describe '#decompress' do %w[ content-encoding Content-Encoding ].each do |header| context "for the #{header} header" do define_method :instance do |body, content_encoding| headers = { 'content-type' => 'text', 'content-length' => body.bytesize.to_s } headers[header] = content_encoding if content_encoding described_class.new(VCR::ResponseStatus.new, headers, body) end let(:content) { 'The quick brown fox jumps over the lazy dog' } it "does nothing when no compression" do resp = instance('Hello', nil) expect(resp).not_to be_compressed expect { expect(resp.decompress).to equal(resp) }.to_not change { resp.headers['content-length'] } end it "does nothing when encoding is 'identity'" do resp = instance('Hello', 'identity') expect(resp).not_to be_compressed expect { expect(resp.decompress).to equal(resp) }.to_not change { resp.headers['content-length'] } end it "raises error for unrecognized encoding" do resp = instance('Hello', 'flabbergaster') expect(resp).not_to be_compressed expect { resp.decompress }. to raise_error(Errors::UnknownContentEncodingError, 'unknown content encoding: flabbergaster') end it "unzips gzipped response" do io = StringIO.new writer = Zlib::GzipWriter.new(io) writer << content writer.close gzipped = io.string resp = instance(gzipped, 'gzip') expect(resp).to be_compressed expect { expect(resp.decompress).to equal(resp) expect(resp).not_to be_compressed expect(resp.body).to eq(content) }.to change { resp.headers['content-length'] }. from([gzipped.bytesize.to_s]). to([content.bytesize.to_s]) end it "inflates deflated response" do deflated = Zlib::Deflate.deflate(content) resp = instance(deflated, 'deflate') expect(resp).to be_compressed expect { expect(resp.decompress).to equal(resp) expect(resp).not_to be_compressed expect(resp.body).to eq(content) }.to change { resp.headers['content-length'] }. from([deflated.bytesize.to_s]). to([content.bytesize.to_s]) end end end end end end vcr-5.0.0/spec/lib/vcr/test_frameworks/000077500000000000000000000000001347305653000200255ustar00rootroot00000000000000vcr-5.0.0/spec/lib/vcr/test_frameworks/cucumber_spec.rb000066400000000000000000000100631347305653000231710ustar00rootroot00000000000000require 'spec_helper' describe VCR::CucumberTags do subject { described_class.new(self) } let(:before_blocks_for_tags) { {} } let(:after_blocks_for_tags) { {} } def scenario(name) double(:name => name, :feature => double(:name => "My feature name\nThe preamble text is not included"), :failed? => false) end let(:current_scenario) { scenario "My scenario name\nThe preamble text is not included" } # define our own Before/After so we can test this in isolation from cucumber's implementation. def Before(tag, &block) before_blocks_for_tags[tag.sub('@', '')] = block end def After(tag, &block) after_blocks_for_tags[tag.sub('@', '')] = block end def test_tag(cassette_attribute, tag, expected_value, scenario=current_scenario) expect(VCR.current_cassette).to be_nil before_blocks_for_tags[tag].call(scenario) expect(VCR.current_cassette.send(cassette_attribute)).to eq(expected_value) after_blocks_for_tags[tag].call(scenario) expect(VCR.current_cassette).to be_nil end %w(tags tag).each do |tag_method| describe "##{tag_method}" do it "creates a cucumber Around hook for each given tag so that the scenario runs with the cassette inserted" do subject.send(tag_method, 'tag1', 'tag2') test_tag(:name, 'tag1', 'cucumber_tags/tag1') test_tag(:name, 'tag2', 'cucumber_tags/tag2') end it "works with tags that start with an @" do subject.send(tag_method, '@tag1', '@tag2') test_tag(:name, 'tag1', 'cucumber_tags/tag1') test_tag(:name, 'tag2', 'cucumber_tags/tag2') end it "passes along the given options to the cassette" do subject.send(tag_method, 'tag1', :record => :none) subject.send(tag_method, 'tag2', :record => :new_episodes) test_tag(:record_mode, 'tag1', :none) test_tag(:record_mode, 'tag2', :new_episodes) end context 'with :use_scenario_name as an option' do it "uses the scenario's name as the cassette name" do subject.send(tag_method, 'tag1', :use_scenario_name => true) test_tag(:name, 'tag1', 'My feature name/My scenario name') end it "makes a unique name for each element of scenario outline" do subject.send(tag_method, 'tag1', :use_scenario_name => true) scenario_with_outline = double(:name => "My row name", :failed? => false, :scenario_outline => double(:feature => double(:name => "My feature name\nThe preamble text is not included"), :name => "My scenario outline name")) test_tag(:name, 'tag1', 'My feature name/My scenario outline name/My row name', scenario_with_outline) end it 'does not pass :use_scenario_name along the given options to the cassette' do subject.send(tag_method, 'tag1', :use_scenario_name => true) expect(VCR::Cassette).to receive(:new).with(anything, hash_not_including(:use_scenario_name)) before_blocks_for_tags['tag1'].call(current_scenario) end it 'does not modify the options passed to the cassette' do original_options = { :use_scenario_name => true, :record => :none } subject.send(tag_method, 'tag1', original_options) before_blocks_for_tags['tag1'].call(current_scenario) expect(original_options.size).to eq(2) expect(original_options[:use_scenario_name]).to eq(true) expect(original_options[:record]).to eq(:none) end it "works properly when multiple scenarios use the tag" do subject.send(tag_method, 'tag1', :use_scenario_name => true) test_tag(:name, 'tag1', 'My feature name/Foo', scenario("Foo")) test_tag(:name, 'tag1', 'My feature name/Bar', scenario("Bar")) end end end end describe '.tags' do it 'returns the list of cucumber tags' do subject.tags 'tag1', 'tag2' subject.tags 'tag3', 'tag4' expect(described_class.tags[-4, 4]).to eq(%w(@tag1 @tag2 @tag3 @tag4)) end end end vcr-5.0.0/spec/lib/vcr/test_frameworks/rspec_spec.rb000066400000000000000000000027521347305653000225060ustar00rootroot00000000000000require 'spec_helper' VCR.configuration.configure_rspec_metadata! describe VCR::RSpec::Metadata, :skip_vcr_reset do before(:all) { VCR.reset! } after(:each) { VCR.reset! } context 'an example group', :vcr do context 'with a nested example group' do it 'uses a cassette for any examples' do expect(VCR.current_cassette.name.split('/')).to eq([ 'VCR::RSpec::Metadata', 'an example group', 'with a nested example group', 'uses a cassette for any examples' ]) end end context 'when the spec has no description' do it do expect(VCR.current_cassette.name.split('/')).to eq([ 'VCR::RSpec::Metadata', 'an example group', 'when the spec has no description', '1:1:2:1' ]) end end end context 'with the cassette name overridden at the example group level', :vcr => { :cassette_name => 'foo' } do it 'overrides the cassette name for an example' do expect(VCR.current_cassette.name).to eq('foo') end it 'overrides the cassette name for another example' do expect(VCR.current_cassette.name).to eq('foo') end end it 'allows the cassette name to be overriden', :vcr => { :cassette_name => 'foo' } do expect(VCR.current_cassette.name).to eq('foo') end it 'allows the cassette options to be set', :vcr => { :match_requests_on => [:method] } do expect(VCR.current_cassette.match_requests_on).to eq([:method]) end end vcr-5.0.0/spec/lib/vcr/util/000077500000000000000000000000001347305653000155635ustar00rootroot00000000000000vcr-5.0.0/spec/lib/vcr/util/hooks_spec.rb000066400000000000000000000107771347305653000202610ustar00rootroot00000000000000require 'spec_helper' describe VCR::Hooks::FilteredHook do describe "#conditionally_invoke" do it 'invokes the hook' do called = false subject.hook = lambda { called = true } subject.conditionally_invoke expect(called).to be true end it 'forwards the given arguments to the hook' do args = nil subject.hook = lambda { |a, b| args = [a, b] } subject.conditionally_invoke(3, 5) expect(args).to eq([3, 5]) end it 'forwards only as many arguments as the hook block accepts' do args = nil subject.hook = lambda { |a| args = [a] } subject.conditionally_invoke(3, 5) expect(args).to eq([3]) end it 'does not invoke the hook if all of the filters return false' do called = false subject.hook = lambda { called = true } subject.filters = lambda { false } subject.conditionally_invoke expect(called).to be false end it 'does not invoke the hook if any of the filters returns false' do called = false subject.hook = lambda { called = true } subject.filters = [lambda { false }, lambda { true }] subject.conditionally_invoke expect(called).to be false end it 'forwards arguments to the filters' do filter_args = nil subject.filters = lambda { |a, b| filter_args = [a, b]; false } subject.conditionally_invoke(3, 5) expect(filter_args).to eq([3, 5]) end it 'handles splat args properly' do filter_args = nil subject.filters = lambda { |*args| filter_args = args; false } subject.conditionally_invoke(3, 5) expect(filter_args).to eq([3, 5]) end it 'forwards only as many arguments as the filter blocks accept' do args1 = args2 = nil subject.filters = [ lambda { |a| args1 = [a]; true }, lambda { |a, b| args2 = [a, b]; false } ] subject.conditionally_invoke(3, 5) expect(args1).to eq([3]) expect(args2).to eq([3, 5]) end it '#to_procs the filter objects' do filter_called = false subject.hook = lambda { } subject.filters = [double(:to_proc => lambda { filter_called = true })] subject.conditionally_invoke expect(filter_called).to be true end end end describe VCR::Hooks do let(:hooks_class) { Class.new { include VCR::Hooks } } subject { hooks_class.new } let(:invocations) { [] } before(:each) do hooks_class.instance_eval do define_hook :before_foo define_hook :before_bar, :prepend end end it 'allows the class to override the hook method and super to the main definition' do override_called = nil hooks_class.class_eval do define_method :before_foo do |&block| override_called = true super(&block) end end subject.before_foo { } expect(override_called).to be true end describe '#clear_hooks' do it 'clears all hooks' do subject.before_foo { invocations << :callback } subject.clear_hooks subject.invoke_hook(:before_foo) expect(invocations).to be_empty end end describe '#invoke_hook' do it 'invokes each of the callbacks' do subject.before_foo { invocations << :callback_1 } subject.before_foo { invocations << :callback_2 } expect(invocations).to be_empty subject.invoke_hook(:before_foo) expect(invocations).to eq([:callback_1, :callback_2]) end it 'maps the return value of each callback' do subject.before_foo { 17 } subject.before_foo { 12 } expect(subject.invoke_hook(:before_foo)).to eq([17, 12]) end it 'does not invoke any filtered callbacks' do subject.before_foo(:real?) { invocations << :blue_callback } subject.invoke_hook(:before_foo, double(:real? => false)) expect(invocations).to be_empty end it 'invokes them in reverse order if the hook was defined with :prepend' do subject.before_bar { 17 } subject.before_bar { 12 } expect(subject.invoke_hook(:before_bar)).to eq([12, 17]) end end describe "#has_hooks_for?" do it 'returns false when given an unrecognized hook name' do expect(subject).not_to have_hooks_for(:abcd) end it 'returns false when given the name of a defined hook that has no registered callbacks' do expect(subject).not_to have_hooks_for(:before_foo) end it 'returns true when given the name of a defined hook that has registered callbacks' do subject.before_foo { } expect(subject).to have_hooks_for(:before_foo) end end end vcr-5.0.0/spec/lib/vcr/util/internet_connection_spec.rb000066400000000000000000000021561347305653000231750ustar00rootroot00000000000000require 'spec_helper' describe VCR::InternetConnection do describe '.available?' do before(:each) do described_class.send(:remove_instance_variable, :@available) if described_class.instance_variable_defined?(:@available) end def stub_pingecho_with(value) allow(VCR::Ping).to receive(:pingecho).with("example.com", anything, anything).and_return(value) end context 'when pinging example.com succeeds' do it 'returns true' do stub_pingecho_with(true) expect(described_class).to be_available end it 'memoizes the value so no extra pings are made' do expect(VCR::Ping).to receive(:pingecho).once.and_return(true) 3.times { described_class.available? } end end context 'when pinging example.com fails' do it 'returns false' do stub_pingecho_with(false) expect(described_class).not_to be_available end it 'memoizes the value so no extra pings are made' do expect(VCR::Ping).to receive(:pingecho).once.and_return(false) 3.times { described_class.available? } end end end end vcr-5.0.0/spec/lib/vcr/util/version_checker_spec.rb000066400000000000000000000021311347305653000222700ustar00rootroot00000000000000require 'spec_helper' module VCR describe VersionChecker do it 'raises an error if the major version is too low' do checker = VersionChecker.new('foo', '0.7.3', '1.0.0') expect { checker.check_version! }.to raise_error(Errors::LibraryVersionTooLowError) end it 'raises an error if the minor version is too low' do checker = VersionChecker.new('foo', '1.0.99', '1.1.3') expect { checker.check_version! }.to raise_error(Errors::LibraryVersionTooLowError) end it 'raises an error if the patch version is too low' do checker = VersionChecker.new('foo', '1.0.8', '1.0.10') expect { checker.check_version! }.to raise_error(Errors::LibraryVersionTooLowError) end it 'does not raise an error when the version is equal' do checker = VersionChecker.new('foo', '1.0.0', '1.0.0') expect { checker.check_version! }.not_to raise_error end it 'does not raise an error when the version is higher' do checker = VersionChecker.new('foo', '2.0.0', '1.0.0') expect { checker.check_version! }.not_to raise_error end end end vcr-5.0.0/spec/lib/vcr/version_spec.rb000066400000000000000000000010061347305653000176270ustar00rootroot00000000000000require 'spec_helper' describe "VCR.version" do subject { VCR.version } it { should =~ /\A\d+\.\d+\.\d+(\.\w+)?\z/ } describe '#parts' do subject { super().parts } it { should be_kind_of(Array) } end describe '#major' do subject { super().major } it { should be_kind_of(Integer) } end describe '#minor' do subject { super().minor } it { should be_kind_of(Integer) } end describe '#patch' do subject { super().patch } it { should be_kind_of(Integer) } end end vcr-5.0.0/spec/lib/vcr_spec.rb000066400000000000000000000272731347305653000161600ustar00rootroot00000000000000require 'spec_helper' describe VCR do def insert_cassette(name = :cassette_test) VCR.insert_cassette(name) end describe '.insert_cassette' do it 'creates a new cassette' do expect(insert_cassette).to be_instance_of(VCR::Cassette) end it 'takes over as the #current_cassette' do orig_cassette = VCR.current_cassette new_cassette = insert_cassette expect(new_cassette).not_to eq(orig_cassette) expect(VCR.current_cassette).to eq(new_cassette) end it 'raises an error if the stack of inserted cassettes already contains a cassette with the same name' do insert_cassette(:foo) expect { insert_cassette(:foo) }.to raise_error(/There is already a cassette with the same name/) end end describe '.eject_cassette' do it 'ejects the current cassette' do cassette = insert_cassette expect(cassette).to receive(:eject) VCR.eject_cassette end it 'forwards the given options to `Cassette#eject`' do cassette = insert_cassette expect(cassette).to receive(:eject).with(:some => :options) VCR.eject_cassette(:some => :options) end it 'returns the ejected cassette' do cassette = insert_cassette expect(VCR.eject_cassette).to eq(cassette) end it 'returns the #current_cassette to the previous one' do cassette1, cassette2 = insert_cassette(:foo1), insert_cassette(:foo2) expect { VCR.eject_cassette }.to change(VCR, :current_cassette).from(cassette2).to(cassette1) end it 'keeps the cassette as the current one until after #eject has finished' do cassette = insert_cassette current = nil allow(cassette).to receive(:eject) { current = VCR.current_cassette } VCR.eject_cassette expect(current).to be(cassette) expect(VCR.current_cassette).not_to be(cassette) end it 'properly pops the cassette off the stack even if an error occurs' do cassette = insert_cassette allow(cassette).to receive(:eject) { raise "boom" } expect { VCR.eject_cassette }.to raise_error("boom") expect(VCR.current_cassette).to be_nil end end describe '.use_cassette' do it 'inserts a new cassette' do new_cassette = VCR::Cassette.new(:use_cassette_test) expect(VCR).to receive(:insert_cassette).and_return(new_cassette) VCR.use_cassette(:cassette_test) { } end it 'yields' do yielded = false VCR.use_cassette(:cassette_test, &lambda { yielded = true }) expect(yielded).to be true end it 'yields the cassette instance if the block expects an argument' do VCR.use_cassette('name', :record => :new_episodes, &lambda do |cassette| expect(cassette).to equal(VCR.current_cassette) end) end it 'yields the cassette instance if the block expects a variable number of args' do VCR.use_cassette('name', :record => :new_episodes) do |*args| expect(args.size).to eq(1) expect(args.first).to equal(VCR.current_cassette) end end it 'ejects the cassette' do expect(VCR).to receive(:eject_cassette) VCR.use_cassette(:cassette_test) { } end it 'ejects the cassette even if there is an error' do expect(VCR).to receive(:eject_cassette) test_error = Class.new(StandardError) expect { VCR.use_cassette(:cassette_test) { raise test_error } }.to raise_error(test_error) end it 'does not eject a cassette if there was an error inserting it' do expect(VCR).to receive(:insert_cassette).and_raise(StandardError.new('Boom!')) expect(VCR).not_to receive(:eject_cassette) expect { VCR.use_cassette(:test) { } }.to raise_error(StandardError, 'Boom!') end it 'raises a helpful error if no block is given' do expect { VCR.use_cassette(:test) }.to raise_error(/requires a block/) end end describe '.http_interactions' do it 'returns the current_cassette.http_interactions when there is a current cassette' do cassette = VCR.insert_cassette("a cassette") expect(VCR.http_interactions).to be(cassette.http_interactions) end it 'returns a null list when there is no current cassette' do expect(VCR.current_cassette).to be_nil expect(VCR.http_interactions).to be(VCR::Cassette::HTTPInteractionList::NullList) end end describe '.real_http_connections_allowed?' do context 'when a cassette is inserted' do it 'returns true if the cassette is recording' do VCR.insert_cassette('foo', :record => :all) expect(VCR.current_cassette).to be_recording expect(VCR.real_http_connections_allowed?).to be true end it 'returns false if the cassette is not recording' do VCR.insert_cassette('foo', :record => :none) expect(VCR.current_cassette).not_to be_recording expect(VCR.real_http_connections_allowed?).to be false end end context 'when no cassette is inserted' do before(:each) do expect(VCR.current_cassette).to be_nil end it 'returns true if the allow_http_connections_when_no_cassette option is set to true' do expect(VCR).to be_turned_on VCR.configure { |c| c.allow_http_connections_when_no_cassette = true } expect(VCR.real_http_connections_allowed?).to be true end it 'returns true if VCR is turned off' do VCR.turn_off! VCR.configure { |c| c.allow_http_connections_when_no_cassette = false } expect(VCR.real_http_connections_allowed?).to be true end it 'returns false if the allow_http_connections_when_no_cassette option is set to false and VCR is turned on' do expect(VCR).to be_turned_on VCR.configure { |c| c.allow_http_connections_when_no_cassette = false } expect(VCR.real_http_connections_allowed?).to be false end end end describe '.request_matchers' do it 'always returns the same memoized request matcher registry instance' do expect(VCR.request_matchers).to be_a(VCR::RequestMatcherRegistry) expect(VCR.request_matchers).to be(VCR.request_matchers) end end describe '.request_ignorer' do it 'always returns the same memoized request ignorer instance' do expect(VCR.request_ignorer).to be_a(VCR::RequestIgnorer) expect(VCR.request_ignorer).to be(VCR.request_ignorer) end end describe '.library_hooks' do it 'always returns the same memoized LibraryHooks instance' do expect(VCR.library_hooks).to be_a(VCR::LibraryHooks) expect(VCR.library_hooks).to be(VCR.library_hooks) end end describe '.cassette_serializers' do it 'always returns the same memoized cassette serializers instance' do expect(VCR.cassette_serializers).to be_a(VCR::Cassette::Serializers) expect(VCR.cassette_serializers).to be(VCR.cassette_serializers) end end describe ".cassette_persisters" do it "always returns the same memoized Cassette::Persisters instance" do expect(VCR.cassette_persisters).to be_a(VCR::Cassette::Persisters) expect(VCR.cassette_persisters).to be(VCR.cassette_persisters) end end describe '.configuration' do it 'returns the configuration object' do expect(VCR.configuration).to be_a(VCR::Configuration) end it 'memoizes the instance' do expect(VCR.configuration).to be(VCR.configuration) end end describe '.configure' do it 'yields the configuration object' do yielded_object = nil VCR.configure do |obj| yielded_object = obj end expect(yielded_object).to eq(VCR.configuration) end end describe '.cucumber_tags' do it 'yields a cucumber tags object' do yielded_object = nil VCR.cucumber_tags do |obj| yielded_object = obj end expect(yielded_object).to be_instance_of(VCR::CucumberTags) end end describe '.record_http_interaction' do before(:each) { allow(VCR).to receive(:current_cassette).and_return(current_cassette) } let(:interaction) { double(:request => double) } context 'when there is not a current cassette' do let(:current_cassette) { nil } it 'does not record a request' do # we can't set a message expectation on nil, but there is no place to record it to... # this mostly tests that there is no error. VCR.record_http_interaction(interaction) end end context 'when there is a current cassette' do let(:current_cassette) { double('current cassette') } it 'records the request when it should not be ignored' do allow(VCR.request_ignorer).to receive(:ignore?).with(interaction.request).and_return(false) expect(current_cassette).to receive(:record_http_interaction).with(interaction) VCR.record_http_interaction(interaction) end it 'does not record the request when it should be ignored' do allow(VCR.request_ignorer).to receive(:ignore?).with(interaction.request).and_return(true) expect(current_cassette).not_to receive(:record_http_interaction) VCR.record_http_interaction(interaction) end end end describe '.turn_off!' do it 'indicates it is turned off' do VCR.turn_off! expect(VCR).not_to be_turned_on end it 'raises an error if a cassette is in use' do VCR.insert_cassette('foo') expect { VCR.turn_off! }.to raise_error(VCR::CassetteInUseError, /foo/) end it 'causes an error to be raised if you insert a cassette while VCR is turned off' do VCR.turn_off! expect { VCR.insert_cassette('foo') }.to raise_error(VCR::TurnedOffError) end it 'raises an ArgumentError when given an invalid option' do expect { VCR.turn_off!(:invalid_option => true) }.to raise_error(ArgumentError) end it 'sets ignore_cassettes to false' do VCR.turn_off! expect(VCR.send(:ignore_cassettes?)).to equal(false) end context 'when `:ignore_cassettes => true` is passed' do before(:each) { VCR.turn_off!(:ignore_cassettes => true) } it 'ignores cassette insertions' do VCR.insert_cassette('foo') expect(VCR.current_cassette).to be_nil end it 'still runs a block passed to use_cassette' do yielded = false VCR.use_cassette('foo') do yielded = true expect(VCR.current_cassette).to be_nil end expect(yielded).to be true end end end describe '.turn_on!' do before(:each) { VCR.turn_off! } it 'indicates it is turned on' do VCR.turn_on! expect(VCR).to be_turned_on end end describe '.turned_off' do it 'yields with VCR turned off' do expect(VCR).to be_turned_on yielded = false VCR.turned_off do yielded = true expect(VCR).not_to be_turned_on end expect(yielded).to eq(true) expect(VCR).to be_turned_on end it 'passes options through to .turn_off!' do expect(VCR).to receive(:turn_off!).with(:ignore_cassettes => true) VCR.turned_off(:ignore_cassettes => true) { } end end describe '.turned_on?' do it 'is on by default' do expect(VCR).to be_turned_on end end describe '.use_cassettes' do it 'uses multiple cassettes' do cassette_by_github = VCR::Cassette.new(:use_cassette_test_call_github) cassette_by_apple = VCR::Cassette.new(:use_cassette_test_call_apple) expect(VCR).to receive(:insert_cassette).with(cassette_by_github, {}).and_return(cassette_by_github) expect(VCR).to receive(:insert_cassette).with(cassette_by_apple, { erb: true }).and_return(cassette_by_apple) cassettes = [ { name: cassette_by_github }, { name: cassette_by_apple, options: { erb: true } } ] VCR.use_cassettes(cassettes) { } end end end vcr-5.0.0/spec/monkey_patches.rb000066400000000000000000000070701347305653000166100ustar00rootroot00000000000000module MonkeyPatches extend self def enable!(scope) case scope when :webmock ::WebMock.reset! ::WebMock::HttpLibAdapters::NetHttpAdapter.enable! ::WebMock::HttpLibAdapters::TyphoeusAdapter.enable! if defined?(::Typhoeus) ::WebMock::HttpLibAdapters::ExconAdapter.enable! if defined?(::Excon) $original_webmock_callbacks.each do |cb| ::WebMock::CallbackRegistry.add_callback(cb[:options], cb[:block]) end when :typhoeus $original_typhoeus_global_hooks.each do |hook| ::Typhoeus.on_complete << hook end ::Typhoeus.before.clear $original_typhoeus_before_hooks.each do |hook| ::Typhoeus.before << hook end when :typhoeus_0_4 ::Typhoeus::Hydra.global_hooks = $original_typhoeus_global_hooks ::Typhoeus::Hydra.stub_finders.clear $original_typhoeus_stub_finders.each do |finder| ::Typhoeus::Hydra.stub_finders << finder end when :excon VCR::LibraryHooks::Excon.configure_middleware else raise ArgumentError.new("Unexpected scope: #{scope}") end end def disable_all! if defined?(::WebMock::HttpLibAdapters) ::WebMock::HttpLibAdapters::NetHttpAdapter.disable! ::WebMock::HttpLibAdapters::TyphoeusAdapter.disable! if defined?(::Typhoeus) ::WebMock::HttpLibAdapters::ExconAdapter.disable! if defined?(::Excon) ::WebMock::CallbackRegistry.reset ::WebMock::StubRegistry.instance.request_stubs = [] end if defined?(::Typhoeus.before) ::Typhoeus.on_complete.clear ::Typhoeus.before.clear elsif defined?(::Typhoeus::Hydra) ::Typhoeus::Hydra.clear_global_hooks ::Typhoeus::Hydra.stub_finders.clear end if defined?(::Excon) ::Excon.defaults[:middlewares].delete(VCR::Middleware::Excon::Request) ::Excon.defaults[:middlewares].delete(VCR::Middleware::Excon::Response) end end end # Require all the HTTP libraries--these must be required before WebMock # for WebMock to work with them. require 'httpclient' if RUBY_INTERPRETER == :mri require 'typhoeus' begin require 'patron' require 'em-http-request' require 'curb' rescue LoadError # these are not always available, depending on the Gemfile used warn $!.message end end if defined?(::Typhoeus.before) require 'vcr/library_hooks/typhoeus' $typhoeus_after_loaded_hook = VCR.configuration.hooks[:after_library_hooks_loaded].last $original_typhoeus_global_hooks = Typhoeus.on_complete.dup $original_typhoeus_before_hooks = Typhoeus.before.dup elsif defined?(::Typhoeus::Hydra.global_hooks) require 'vcr/library_hooks/typhoeus' $typhoeus_0_4_after_loaded_hook = VCR.configuration.hooks[:after_library_hooks_loaded].first $typhoeus_after_loaded_hook = VCR.configuration.hooks[:after_library_hooks_loaded].last $original_typhoeus_global_hooks = Typhoeus::Hydra.global_hooks.dup $original_typhoeus_stub_finders = Typhoeus::Hydra.stub_finders.dup end require 'vcr/library_hooks/webmock' $original_webmock_callbacks = ::WebMock::CallbackRegistry.callbacks require 'vcr/library_hooks/excon' $excon_after_loaded_hook = VCR.configuration.hooks[:after_library_hooks_loaded].last # disable all by default; we'll enable specific ones when we need them MonkeyPatches.disable_all! RSpec.configure do |config| [:webmock, :typhoeus, :typhoeus_0_4, :excon].each do |scope| config.before(:all, :with_monkey_patches => scope) { MonkeyPatches.enable!(scope) } config.after(:all, :with_monkey_patches => scope) { MonkeyPatches.disable_all! } end end vcr-5.0.0/spec/spec_helper.rb000066400000000000000000000032341347305653000160660ustar00rootroot00000000000000require "codeclimate-test-reporter" CodeClimate::TestReporter.start require "pry" require "rspec" require "vcr" require "date" require "forwardable" require "uri" require "vcr/util/internet_connection" require_relative "support/integer_extension" require_relative "support/limited_uri" require_relative "support/ruby_interpreter" require_relative "support/shared_example_groups/hook_into_http_library" require_relative "support/shared_example_groups/request_hooks" require_relative "support/vcr_stub_helpers" require_relative "support/vcr_localhost_server" require_relative "support/sinatra_app" require_relative "monkey_patches" require_relative "support/http_library_adapters" module VCR SPEC_ROOT = File.dirname(File.expand_path('.', __FILE__)) def reset!(hook = nil) instance_variables.each do |ivar| instance_variable_set(ivar, nil) end initialize_ivars configuration.hook_into hook if hook end end RSpec.configure do |config| tmp_dir = File.expand_path('../../tmp/cassette_library_dir', __FILE__) config.before(:each) do |example| unless example.metadata[:skip_vcr_reset] VCR.reset! VCR.configuration.cassette_library_dir = tmp_dir VCR.configuration.uri_parser = LimitedURI end end config.after(:each) do FileUtils.rm_rf tmp_dir end config.before(:all, :disable_warnings => true) do @orig_std_err = $stderr $stderr = StringIO.new end config.after(:all, :disable_warnings => true) do $stderr = @orig_std_err end config.filter_run :focus => true config.run_all_when_everything_filtered = true config.alias_it_should_behave_like_to :it_performs, 'it performs' end VCR::SinatraApp.boot vcr-5.0.0/spec/support/000077500000000000000000000000001347305653000147625ustar00rootroot00000000000000vcr-5.0.0/spec/support/configuration_stubbing.rb000066400000000000000000000002501347305653000220500ustar00rootroot00000000000000shared_context "configuration stubbing" do let(:config) { double("VCR::Configuration") } before do allow(VCR).to receive(:configuration) { config } end end vcr-5.0.0/spec/support/cucumber_helpers.rb000066400000000000000000000017111347305653000206360ustar00rootroot00000000000000require "rubygems" require "vcr" require "support/integer_extension" module Gem def self.win_platform?() false end end unless defined?(Gem) # pretend we're always on the internet (so that we don't have an # internet connection dependency for our cukes) VCR::InternetConnection.class_eval do def available?; true; end end if ENV['DATE_STRING'] require 'timecop' Timecop.travel(Date.parse(ENV['DATE_STRING'])) end def include_http_adapter_for(lib) require((lib =~ /faraday/) ? 'faraday' : lib) require 'typhoeus' if lib.include?('typhoeus') # for faraday-typhoeus require 'support/http_library_adapters' include HTTP_LIBRARY_ADAPTERS[lib] end def response_body_for(*args) get_body_string(make_http_request(*args)) end def start_sinatra_app(&block) require 'sinatra/base' require 'support/vcr_localhost_server' klass = Class.new(Sinatra::Base) klass.disable :protection klass.class_eval(&block) VCR::LocalhostServer.new(klass.new) end vcr-5.0.0/spec/support/http_library_adapters.rb000066400000000000000000000174251347305653000217060ustar00rootroot00000000000000module HeaderDowncaser def downcase_headers(headers) {}.tap do |downcased| headers.each do |k, v| downcased[k.downcase] = v end end end end HTTP_LIBRARY_ADAPTERS = {} HTTP_LIBRARY_ADAPTERS['net/http'] = Module.new do include HeaderDowncaser def self.http_library_name; 'Net::HTTP'; end def get_body_string(response); response.body; end alias get_body_object get_body_string def get_header(header_key, response) response.get_fields(header_key) end def make_http_request(method, url, body = nil, headers = {}) uri = URI.parse(url) http = Net::HTTP.new(uri.host, uri.port) if uri.scheme == "https" http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE end http.send_request(method.to_s.upcase, uri.request_uri, body, headers) end DEFAULT_REQUEST_HEADERS = { "Accept"=>["*/*"] } DEFAULT_REQUEST_HEADERS['User-Agent'] = ["Ruby"] DEFAULT_REQUEST_HEADERS['Accept-Encoding'] = ["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"] if RUBY_VERSION.to_f > 1.9 def normalize_request_headers(headers) defined?(super) ? super : downcase_headers(headers.merge(DEFAULT_REQUEST_HEADERS)) end end HTTP_LIBRARY_ADAPTERS['patron'] = Module.new do def self.http_library_name; 'Patron'; end def get_body_string(response); response.body; end alias get_body_object get_body_string def get_header(header_key, response) response.headers[header_key] end def make_http_request(method, url, body = nil, headers = {}) Patron::Session.new.request(method, url, headers, :data => body || '') end def normalize_request_headers(headers) headers.merge('Expect' => ['']) end end HTTP_LIBRARY_ADAPTERS['httpclient'] = Module.new do def self.http_library_name; 'HTTP Client'; end def get_body_string(response) body = response.body string = body.is_a?(String) ? body : body.content string.respond_to?(:read) ? string.read : string end def get_body_object(response) response.body end def get_header(header_key, response) response.header[header_key] end def make_http_request(method, url, body = nil, headers = {}) HTTPClient.new.request(method, url, nil, body, headers) end def normalize_request_headers(headers) headers.merge({ 'Accept' => ["*/*"], 'User-Agent' => ["HTTPClient/1.0 (#{HTTPClient::VERSION}, ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}))"], 'Date' => [Time.now.httpdate] }) end end HTTP_LIBRARY_ADAPTERS['em-http-request'] = Module.new do def self.http_library_name; 'EM HTTP Request'; end def get_body_string(response) response.response end alias get_body_object get_body_string def get_header(header_key, response) values = response.response_header[header_key.upcase.gsub('-', '_')] values.is_a?(Array) ? values : values.split(', ') end def make_http_request(method, url, body = nil, headers = {}) http = nil EventMachine.run do http = EventMachine::HttpRequest.new(url).send(method, :body => body, :head => headers) http.callback { EventMachine.stop } end http end def normalize_request_headers(headers) headers end end HTTP_LIBRARY_ADAPTERS['curb'] = Module.new do def self.http_library_name; "Curb"; end def get_body_string(response) response.body_str end alias get_body_object get_body_string def get_header(header_key, response) headers = response.header_str.split("\r\n")[1..-1] value = nil headers.each do |h| next unless h =~ /^#{Regexp.escape(header_key)}: (.*)$/ new_value = $1.split(', ') value = value ? Array(value) + Array(new_value) : new_value end value end def make_http_request(method, url, body = nil, headers = {}) Curl::Easy.new(url) do |c| c.headers = headers if [:post, :put].include?(method) c.send("http_#{method}", body) else c.send("http_#{method}") end end end def normalize_request_headers(headers) headers end end HTTP_LIBRARY_ADAPTERS['typhoeus'] = Module.new do def self.http_library_name; "Typhoeus"; end def get_body_string(response) response.body end alias get_body_object get_body_string def get_header(header_key, response) # Due to https://github.com/typhoeus/typhoeus/commit/256c95473d5d40d7ec2f5db603687323ddd73689 # headers are now downcased. # ...except when they're not. I'm not 100% why (I haven't had time to dig into it yet) # but in some situations the headers aren't downcased. I think it has to do with playback; VCR # isn't sending the headers in downcased to typhoeus. It gets complicated with the interaction # w/ WebMock, and the fact that webmock normalizes headers in a different fashion. # # For now this hack works. response.headers.fetch(header_key.downcase) { response.headers[header_key] } end def make_http_request(method, url, body = nil, headers = {}) request = Typhoeus::Request.new(url, :method => method, :body => body, :headers => headers) request.run request.response end def normalize_request_headers(headers) headers.merge("User-Agent"=>["Typhoeus - https://github.com/typhoeus/typhoeus"], 'Expect' => ['']) end end HTTP_LIBRARY_ADAPTERS['typhoeus 0.4'] = Module.new do def self.http_library_name; "Typhoeus"; end def get_body_string(response) response.body end alias get_body_object get_body_string def get_header(header_key, response) response.headers_hash[header_key] end def make_http_request(method, url, body = nil, headers = {}) Typhoeus::Request.send(method, url, :body => body, :headers => headers) end def normalize_request_headers(headers) headers end end HTTP_LIBRARY_ADAPTERS['excon'] = Module.new do def self.http_library_name; "Excon"; end def get_body_string(response) response.body end alias get_body_object get_body_string def get_header(header_key, response) response.headers[header_key] end def make_http_request(method, url, body = nil, headers = {}) # There are multiple ways to use Excon but this is how fog (the main user of excon) uses it: # https://github.com/fog/fog/blob/v1.1.1/lib/fog/aws/rds.rb#L139-147 Excon.new(url).request(:method => method.to_s.upcase, :body => body, :headers => headers) end def normalize_request_headers(headers) headers.merge('User-Agent' => [Excon::USER_AGENT]) end end %w[ net_http typhoeus patron ].each do |_faraday_adapter| if _faraday_adapter == 'typhoeus' && defined?(::Typhoeus::VERSION) && ::Typhoeus::VERSION.to_f >= 0.5 require 'typhoeus/adapters/faraday' end HTTP_LIBRARY_ADAPTERS["faraday (w/ #{_faraday_adapter})"] = Module.new do class << self; self; end.class_eval do define_method(:http_library_name) do "Faraday (#{_faraday_adapter})" end end define_method(:faraday_adapter) { _faraday_adapter.to_sym } def get_body_string(response) response.body end alias get_body_object get_body_string def get_header(header_key, response) value = response.headers[header_key] value.split(', ') if value end def make_http_request(method, url, body = nil, headers = {}) url_root, url_rest = split_url(url) faraday_connection(url_root).send(method) do |req| req.url url_rest headers.each { |k, v| req[k] = v } req.body = body if body end end def split_url(url) uri = URI.parse(url) url_root = "#{uri.scheme}://#{uri.host}:#{uri.port}" rest = url.sub(url_root, '') [url_root, rest] end def faraday_connection(url_root) Faraday::Connection.new(:url => url_root) do |builder| builder.adapter faraday_adapter end end def normalize_request_headers(headers) headers.merge("User-Agent" => ["Faraday v#{Faraday::VERSION}"]) end end end vcr-5.0.0/spec/support/integer_extension.rb000066400000000000000000000002251347305653000210370ustar00rootroot00000000000000unless 7.respond_to?(:days) class Integer def days self * 24 * # hours 60 * # minutes 60 # seconds end end end vcr-5.0.0/spec/support/limited_uri.rb000066400000000000000000000006261347305653000176210ustar00rootroot00000000000000class LimitedURI extend Forwardable def_delegators :@uri, :scheme, :host, :port, :port=, :path, :query, :query=, :to_s def initialize(uri) @uri = uri end def ==(other) to_s == other.to_s end def self.parse(uri) return uri if uri.is_a? LimitedURI return new(uri) if uri.is_a? URI return new(URI.parse(uri)) if uri.is_a? String raise URI::InvalidURIError end end vcr-5.0.0/spec/support/ruby_interpreter.rb000066400000000000000000000002071347305653000207120ustar00rootroot00000000000000RUBY_INTERPRETER = if RUBY_PLATFORM == 'java' :jruby elsif defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx' :rubinius else :mri end vcr-5.0.0/spec/support/shared_example_groups/000077500000000000000000000000001347305653000213425ustar00rootroot00000000000000vcr-5.0.0/spec/support/shared_example_groups/excon.rb000066400000000000000000000040441347305653000230050ustar00rootroot00000000000000shared_examples "Excon streaming" do context "when Excon's streaming API is used" do def make_request_to(path) chunks = [] Excon.get "http://localhost:#{VCR::SinatraApp.port}#{path}", :response_block => lambda { |chunk, remaining_bytes, total_bytes| chunks << chunk } chunks.join end it 'properly records and plays back the response' do allow(VCR).to receive(:real_http_connections_allowed?).and_return(true) recorded, played_back = [1, 2].map do make_request_to('/foo') end expect(recorded).to eq(played_back) expect(recorded).to eq("FOO!") end it 'properly records and plays back the response for unexpected status' do allow(VCR).to receive(:real_http_connections_allowed?).and_return(true) recorded, played_back = [1, 2].map do chunks = [] VCR.use_cassette('excon_streaming_error', :record => :once) do begin Excon.get "http://localhost:#{VCR::SinatraApp.port}/404_not_200", :expects => 200, :response_block => lambda { |chunk, remaining_bytes, total_bytes| chunks << chunk } rescue Excon::Errors::Error => e chunks << e.response.body end end chunks.join end expect(recorded).to eq(played_back) expect(recorded).to eq('404 not 200') end context "when a cassette is played back and appended to" do it 'does not allow Excon to mutate the response body in the cassette' do VCR.use_cassette('excon_streaming', :record => :new_episodes) do expect(make_request_to('/')).to eq('GET to root') end VCR.use_cassette('excon_streaming', :record => :new_episodes) do expect(make_request_to('/')).to eq('GET to root') expect(make_request_to('/foo')).to eq('FOO!') # so it will save to disk again end VCR.use_cassette('excon_streaming', :record => :new_episodes) do expect(make_request_to('/')).to eq('GET to root') end end end end end vcr-5.0.0/spec/support/shared_example_groups/hook_into_http_library.rb000066400000000000000000000546271347305653000264610ustar00rootroot00000000000000require 'cgi' NET_CONNECT_NOT_ALLOWED_ERROR = /An HTTP request has been made that VCR does not know how to handle/ shared_examples_for "a hook into an HTTP library" do |library_hook_name, library, *other| include HeaderDowncaser include VCRStubHelpers unless adapter_module = HTTP_LIBRARY_ADAPTERS[library] raise ArgumentError.new("No http library adapter module could be found for #{library}") end http_lib_unsupported = (RUBY_INTERPRETER != :mri && library =~ /(typhoeus|curb|patron|em-http)/) describe "using #{adapter_module.http_library_name}", :unless => http_lib_unsupported do include adapter_module # Necessary for ruby 1.9.2. On 1.9.2 we get an error when we use super, # so this gives us another alias we can use for the original method. alias make_request make_http_request 1.upto(2) do |header_count| describe "making an HTTP request that responds with #{header_count} Set-Cookie header(s)" do define_method :get_set_cookie_header do VCR.use_cassette('header_test', :record => :once) do get_header 'Set-Cookie', make_http_request(:get, "http://localhost:#{VCR::SinatraApp.port}/set-cookie-headers/#{header_count}") end end it 'returns the same header value when recording and replaying' do expect((recorded_val = get_set_cookie_header)).not_to be_nil replayed_val = get_set_cookie_header expect(replayed_val).to eq(recorded_val) end end end def self.test_record_and_playback(description, query) describe "a request to a URL #{description}" do define_method :get_body do VCR.use_cassette('record_and_playback', :record => :once) do get_body_string make_http_request(:get, "http://localhost:#{VCR::SinatraApp.port}/record-and-playback?#{query}") end end it "properly records and playsback a request with a URL #{description}" do recorded_body = get_body played_back_body = get_body expect(played_back_body).to eq(recorded_body) end end end test_record_and_playback "with spaces encoded as +", "q=a+b" test_record_and_playback "with spaces encoded as %20", "q=a%20b" test_record_and_playback "with a complex escaped query param", "q=#{CGI.escape("A&(! 234k !@ kasdj232\#$ kjw35")}" it 'plays back an empty body response exactly as it was recorded (e.g. nil vs empty string)' do skip "Faraday 0.8 may return nil bodies" if library_hook_name == :faraday && !defined?(::Faraday::RackBuilder) get_body = lambda do VCR.use_cassette('empty_body', :record => :once) do get_body_object make_http_request(:get, "http://localhost:#{VCR::SinatraApp.port}/204") end end recorded = get_body.call played_back = get_body.call expect(played_back).to eq(recorded) end describe 'making an HTTP request' do let(:status) { VCR::ResponseStatus.new(200, 'OK') } let(:interaction) { VCR::HTTPInteraction.new(request, response) } let(:response_body) { "The response body" } before(:each) do stub_requests([interaction], [:method, :uri]) end context "when the the stubbed request and response has no headers" do let(:request) { VCR::Request.new(:get, 'http://example.com:80/') } let(:response) { VCR::Response.new(status, nil, response_body, '1.1') } it 'returns the response for a matching request' do expect(get_body_string(make_http_request(:get, 'http://example.com/'))).to eq(response_body) end end def self.test_playback(description, url) context "when a URL #{description} has been stubbed" do let(:request) { VCR::Request.new(:get, url) } let(:response) { VCR::Response.new(status, nil, response_body, '1.1') } it 'returns the expected response for the same request' do expect(get_body_string(make_http_request(:get, url))).to eq(response_body) end end end test_playback "using https and no explicit port", "https://example.com/foo" test_playback "using https and port 443", "https://example.com:443/foo" test_playback "using https and some other port", "https://example.com:5190/foo" test_playback "that has query params", "http://example.com/search?q=param" test_playback "with an encoded ampersand", "http://example.com:80/search?q=#{CGI.escape("Q&A")}" end it 'does not query the http interaction list excessively' do call_count = 0 [:has_interaction_matching?, :response_for].each do |method_name| orig_meth = VCR.http_interactions.method(method_name) allow(VCR.http_interactions).to receive(method_name) do |*args| call_count += 1 orig_meth.call(*args) end end VCR.insert_cassette('foo') make_http_request(:get, "http://localhost:#{VCR::SinatraApp.port}/foo") expect(call_count).to eq(1) end describe "using the library's stubbing/disconnection APIs" do let!(:request_url) { "http://localhost:#{VCR::SinatraApp.port}/foo" } if method_defined?(:disable_real_connections) it 'can make a real request when VCR is turned off' do enable_real_connections VCR.turn_off! expect(get_body_string(make_http_request(:get, request_url))).to eq("FOO!") end it 'does not mess with VCR when real connections are disabled' do VCR.insert_cassette('example') disable_real_connections expect(VCR).to receive(:record_http_interaction) do |interaction| expect(interaction.request.uri).to eq(request_url) end make_http_request(:get, request_url) end it 'can disable real connections when VCR is turned off' do VCR.turn_off! expected_error = disable_real_connections expect { make_http_request(:get, request_url) }.to raise_error(expected_error) end end if method_defined?(:directly_stub_request) it 'can directly stub the request when VCR is turned off' do VCR.turn_off! directly_stub_request(:get, request_url, "stubbed response") expect(get_body_string(make_http_request(:get, request_url))).to eq("stubbed response") end it 'can directly stub the request when VCR is turned on and no cassette is in use' do directly_stub_request(:get, request_url, "stubbed response") expect(get_body_string(make_http_request(:get, request_url))).to eq("stubbed response") end it 'can directly stub the request when VCR is turned on and a cassette is in use' do VCR.use_cassette("temp") do directly_stub_request(:get, request_url, "stubbed response") expect(get_body_string(make_http_request(:get, request_url))).to eq("stubbed response") end end it 'does not record requests that are directly stubbed' do expect(VCR).to respond_to(:record_http_interaction) expect(VCR).not_to receive(:record_http_interaction) VCR.use_cassette("temp") do directly_stub_request(:get, request_url, "stubbed response") expect(get_body_string(make_http_request(:get, request_url))).to eq("stubbed response") end end end end describe "request hooks" do context 'when there is an around_http_request hook' do let(:request_url) { "http://localhost:#{VCR::SinatraApp.port}/foo" } it 'yields the request to the block' do yielded_request = nil VCR.configuration.around_http_request do |request| yielded_request = request request.proceed end VCR.use_cassette('new_cassette') do make_http_request(:get, request_url) end expect(yielded_request.method).to eq(:get) expect(yielded_request.uri).to eq(request_url) end it 'returns the response from request.proceed' do response = nil VCR.configuration.around_http_request do |request| response = request.proceed end VCR.use_cassette('new_cassette') do make_http_request(:get, request_url) end expect(response.body).to eq("FOO!") end it 'can be used to use a cassette for a request' do VCR.configuration.around_http_request do |request| VCR.use_cassette('new_cassette', &request) end expect(VCR).to receive(:record_http_interaction) do expect(VCR.current_cassette.name).to eq('new_cassette') end expect(VCR.current_cassette).to be_nil make_http_request(:get, request_url) expect(VCR.current_cassette).to be_nil end it 'nests them inside each other, making the first declared hook the outermost' do order = [] VCR.configure do |c| c.ignore_request { |r| true } c.around_http_request do |request| order << :before_1 request.proceed order << :after_1 end c.around_http_request do |request| order << :before_2 request.proceed order << :after_2 end end make_http_request(:get, request_url) expect(order).to eq([:before_1, :before_2, :after_2, :after_1]) end it 'raises an appropriate error if the hook does not call request.proceed' do VCR.configuration.ignore_request { |r| true } hook_declaration = "#{__FILE__}:#{__LINE__ + 1}" VCR.configuration.around_http_request { |r| } expect { make_http_request(:get, request_url) }.to raise_error { |error| expect(error.message).to include('must call #proceed on the yielded request') expect(error.message).to include(hook_declaration) } end it 'does not get a dead fiber error when multiple requests are made' do VCR.configuration.around_http_request do |request| VCR.use_cassette('new_cassette', &request) end 3.times { make_http_request(:get, request_url) } end it 'allows the hook to be filtered' do order = [] VCR.configure do |c| c.ignore_request { |r| true } c.around_http_request(lambda { |r| r.uri =~ /foo/}) do |request| order << :before_foo request.proceed order << :after_foo end c.around_http_request(lambda { |r| r.uri !~ /foo/}) do |request| order << :before_not_foo request.proceed order << :after_not_foo end end make_http_request(:get, request_url) expect(order).to eq([:before_foo, :after_foo]) end it 'ensures that both around/before are invoked or neither' do order = [] allow_1, allow_2 = false, true VCR.configure do |c| c.ignore_request { |r| true } c.around_http_request(lambda { |r| allow_1 = !allow_1 }) do |request| order << :before_1 request.proceed order << :after_1 end c.around_http_request(lambda { |r| allow_2 = !allow_2 }) do |request| order << :before_2 request.proceed order << :after_2 end end make_http_request(:get, request_url) expect(order).to eq([:before_1, :after_1]) end end if RUBY_VERSION >= '1.9' it 'correctly assigns the correct type to both before and after request hooks, even if they are different' do before_type = after_type = nil VCR.configuration.before_http_request do |request| before_type = request.type VCR.insert_cassette('example') end VCR.configuration.after_http_request do |request| after_type = request.type VCR.eject_cassette end make_http_request(:get, "http://localhost:#{VCR::SinatraApp.port}/foo") expect(before_type).to be(:unhandled) expect(after_type).to be(:recordable) end context "when the request is ignored" do before(:each) do VCR.configuration.ignore_request { |r| true } end it_behaves_like "request hooks", library_hook_name, :ignored end context "when the request is directly stubbed" do before(:each) do directly_stub_request(:get, request_url, "FOO!") end it_behaves_like "request hooks", library_hook_name, :externally_stubbed end if method_defined?(:directly_stub_request) context 'when the request is recorded' do let!(:inserted_cassette) { VCR.insert_cassette('new_cassette') } it_behaves_like "request hooks", library_hook_name, :recordable do let(:string_in_cassette) { 'example.com get response 1 with path=foo' } it 'plays back the cassette when a request is made' do VCR.eject_cassette VCR.configure do |c| c.cassette_library_dir = File.join(VCR::SPEC_ROOT, 'fixtures') c.before_http_request do |request| VCR.insert_cassette('fake_example_responses', :record => :none) end end expect(get_body_string(make_http_request(:get, 'http://example.com/foo'))).to eq(string_in_cassette) end specify 'the after_http_request hook can be used to eject a cassette after the request is recorded' do VCR.configuration.after_http_request { |request| VCR.eject_cassette } expect(VCR).to receive(:record_http_interaction) do |interaction| expect(VCR.current_cassette).to be(inserted_cassette) end make_request expect(VCR.current_cassette).to be_nil end end end context 'when a stubbed response is played back for the request' do before(:each) do stub_requests([http_interaction(request_url)], [:method, :uri]) end it_behaves_like "request hooks", library_hook_name, :stubbed_by_vcr end context 'when the request is not allowed' do it_behaves_like "request hooks", library_hook_name, :unhandled do undef assert_expected_response def assert_expected_response(response) expect(response).to be_nil end undef make_request def make_request(disabled = false) if disabled make_http_request(:get, request_url) else expect { make_http_request(:get, request_url) }.to raise_error(NET_CONNECT_NOT_ALLOWED_ERROR) end end end end end describe '.stub_requests using specific match_attributes' do before(:each) { allow(VCR).to receive(:real_http_connections_allowed?).and_return(false) } let(:interactions) { interactions_from('match_requests_on.yml') } let(:normalized_interactions) do interactions.each do |i| i.request.headers = normalize_request_headers(i.request.headers) end interactions end def self.matching_on(attribute, valid, invalid, &block) describe ":#{attribute}" do let(:perform_stubbing) { stub_requests(normalized_interactions, [attribute]) } before(:each) { perform_stubbing } module_eval(&block) valid.each do |val, response| it "returns the expected response for a #{val.inspect} request" do expect(get_body_string(make_http_request(val))).to eq(response) end end it "raises an error for a request with a different #{attribute}" do expect { make_http_request(invalid) }.to raise_error(NET_CONNECT_NOT_ALLOWED_ERROR) end end end matching_on :method, { :get => "get method response", :post => "post method response" }, :put do def make_http_request(http_method) make_request(http_method, 'http://some-wrong-domain.com/', nil, {}) end end matching_on :host, { 'example1.com' => 'example1.com host response', 'example2.com' => 'example2.com host response' }, 'example3.com' do def make_http_request(host) make_request(:get, "http://#{host}/some/wrong/path", nil, {}) end end matching_on :path, { '/path1' => 'path1 response', '/path2' => 'path2 response' }, '/path3' do def make_http_request(path) make_request(:get, "http://some.wrong.domain.com#{path}?p=q", nil, {}) end end matching_on :uri, { 'http://example.com/uri1' => 'uri1 response', 'http://example.com/uri2' => 'uri2 response' }, 'http://example.com/uri3' do def make_http_request(uri) make_request(:get, uri, nil, {}) end end matching_on :body, { 'param=val1' => 'val1 body response', 'param=val2' => 'val2 body response' }, 'param=val3' do def make_http_request(body) make_request(:put, "http://wrong-domain.com/wrong/path", body, {}) end end matching_on :headers, {{ 'X-Http-Header1' => 'val1' } => 'val1 header response', { 'X-Http-Header1' => 'val2' } => 'val2 header response' }, { 'X-Http-Header1' => 'val3' } do def make_http_request(headers) make_request(:get, "http://wrong-domain.com/wrong/path", nil, headers) end end end def self.test_real_http_request(http_allowed, *other) let(:url) { "http://localhost:#{VCR::SinatraApp.port}/foo" } if http_allowed it 'allows real http requests' do expect(get_body_string(make_http_request(:get, url))).to eq('FOO!') end describe 'recording new http requests' do let(:recorded_interaction) do interaction = nil expect(VCR).to receive(:record_http_interaction) { |i| interaction = i } make_http_request(:post, url, "the body", { 'X-Http-Foo' => 'bar' }) interaction end it 'does not record the request if the hook is disabled' do VCR.library_hooks.exclusively_enabled :something_else do expect(VCR).not_to receive(:record_http_interaction) make_http_request(:get, url) end end it 'records the request uri' do expect(recorded_interaction.request.uri).to eq(url) end it 'records the request method' do expect(recorded_interaction.request.method).to eq(:post) end it 'records the request body' do expect(recorded_interaction.request.body).to eq("the body") end it 'records the request headers' do headers = downcase_headers(recorded_interaction.request.headers) expect(headers).to include('x-http-foo' => ['bar']) end it 'records the response status code' do expect(recorded_interaction.response.status.code).to eq(200) end it 'records the response status message' do expect(recorded_interaction.response.status.message.strip).to eq('OK') end unless other.include?(:status_message_not_exposed) it 'records the response body' do expect(recorded_interaction.response.body).to eq('FOO!') end it 'records the response headers' do headers = downcase_headers(recorded_interaction.response.headers) expect(headers).to include('content-type' => ["text/html;charset=utf-8"]) end end else it 'does not allow real HTTP requests or record them' do expect(VCR).to receive(:record_http_interaction).never expect { make_http_request(:get, url) }.to raise_error(NET_CONNECT_NOT_ALLOWED_ERROR) end end end [true, false].each do |http_allowed| context "when VCR.real_http_connections_allowed? is returning #{http_allowed}" do before(:each) { allow(VCR).to receive(:real_http_connections_allowed?).and_return(http_allowed) } test_real_http_request(http_allowed, *other) unless http_allowed localhost_response = "Localhost response" context 'when ignore_hosts is configured to "127.0.0.1", "localhost"' do before(:each) do VCR.configure { |c| c.ignore_hosts "127.0.0.1", "localhost" } end %w[ 127.0.0.1 localhost ].each do |localhost_alias| it "allows requests to #{localhost_alias}" do expect(get_body_string(make_http_request(:get, "http://#{localhost_alias}:#{VCR::SinatraApp.port}/localhost_test"))).to eq(localhost_response) end end it 'does not allow requests to 0.0.0.0' do expect { make_http_request(:get, "http://0.0.0.0:#{VCR::SinatraApp.port}/localhost_test") }.to raise_error(NET_CONNECT_NOT_ALLOWED_ERROR) end end end context 'when some requests are stubbed' do let(:interactions) { interactions_from('fake_example_responses.yml') } before(:each) do stub_requests(interactions, VCR::RequestMatcherRegistry::DEFAULT_MATCHERS) end it 'gets the stubbed responses when requests are made to http://example.com/foo, and does not record them' do expect(VCR).to receive(:record_http_interaction).never expect(get_body_string(make_http_request(:get, 'http://example.com/foo'))).to match(/example\.com get response \d with path=foo/) end it 'rotates through multiple responses for the same request' do expect(get_body_string(make_http_request(:get, 'http://example.com/foo'))).to eq('example.com get response 1 with path=foo') expect(get_body_string(make_http_request(:get, 'http://example.com/foo'))).to eq('example.com get response 2 with path=foo') end unless other.include?(:does_not_support_rotating_responses) it "correctly handles stubbing multiple values for the same header" do header = get_header('Set-Cookie', make_http_request(:get, 'http://example.com/two_set_cookie_headers')) header = header.split(', ') if header.respond_to?(:split) expect(header).to match_array ['bar=bazz', 'foo=bar'] end end end end end end vcr-5.0.0/spec/support/shared_example_groups/request_hooks.rb000066400000000000000000000033561347305653000245710ustar00rootroot00000000000000shared_examples_for "request hooks" do |library_hook_name, request_type| let(:request_url) { "http://localhost:#{VCR::SinatraApp.port}/foo" } def make_request(disabled = false) make_http_request(:get, request_url) end def assert_expected_response(response) expect(response.status.code).to eq(200) expect(response.body).to eq('FOO!') end [:before_http_request, :after_http_request].each do |hook| specify "the #{hook} hook is only called once per request" do call_count = 0 VCR.configuration.send(hook) { |r| call_count += 1 } make_request expect(call_count).to eq(1) end specify "the #{hook} hook yields the request" do request = nil VCR.configuration.send(hook) { |r| request = r } make_request expect(request.method).to be(:get) expect(request.uri).to eq(request_url) end specify "the #{hook} hook is not called if the library hook is disabled" do expect(VCR.library_hooks).to respond_to(:disabled?) allow(VCR.library_hooks).to receive(:disabled?).and_return(true) hook_called = false VCR.configuration.send(hook) { |r| hook_called = true } make_request(:disabled) expect(hook_called).to be false end specify "the #type of the yielded request given to the #{hook} hook is #{request_type}" do request = nil VCR.configuration.send(hook) { |r| request = r } make_request expect(request.type).to be(request_type) end end specify "the after_http_request hook yields the response if there is one and the second block arg is given" do response = nil VCR.configuration.after_http_request { |req, res| response = res } make_request assert_expected_response(response) end end vcr-5.0.0/spec/support/sinatra_app.rb000066400000000000000000000031011347305653000176030ustar00rootroot00000000000000require 'sinatra' module VCR class SinatraApp < ::Sinatra::Base disable :protection get '/' do "GET to root" end get '/search' do "query: #{params[:q]}" end get '/localhost_test' do "Localhost response" end get '/foo' do "FOO!" end get '/redirect-to-root' do redirect to('/') end post '/foo' do "FOO!" end post '/return-request-body' do request.body end get '/set-cookie-headers/1' do headers 'Set-Cookie' => 'foo' 'header set' end get '/set-cookie-headers/2' do headers 'Set-Cookie' => %w[ bar foo ] 'header set' end get '/204' do status 204 end get '/404_not_200' do status 404 '404 not 200' end # we use a global counter so that every response is different; # this ensures that the test demonstrates that the response # is being played back (and not running a 2nd real request) $record_and_playback_response_count ||= 0 get '/record-and-playback' do "Response #{$record_and_playback_response_count += 1}" end post '/record-and-playback' do "Response #{$record_and_playback_response_count += 1}" end @_boot_failed = false class << self def port server.port end def server raise "Sinatra app failed to boot." if @_boot_failed @server ||= begin VCR::LocalhostServer.new(new) rescue @_boot_failed = true raise end end alias boot server end end end vcr-5.0.0/spec/support/vcr_localhost_server.rb000066400000000000000000000040221347305653000215350ustar00rootroot00000000000000require 'rack' require 'rack/handler/webrick' require 'net/http' # The code for this is inspired by Capybara's server: # http://github.com/jnicklas/capybara/blob/0.3.9/lib/capybara/server.rb module VCR class LocalhostServer READY_MESSAGE = "VCR server ready" class Identify def initialize(app) @app = app end def call(env) if env["PATH_INFO"] == "/__identify__" [200, {}, [VCR::LocalhostServer::READY_MESSAGE]] else @app.call(env) end end end attr_reader :port def initialize(rack_app, port = nil) @port = port || find_available_port @rack_app = rack_app concurrently { boot } wait_until(30, "Boot failed.") { booted? } end private def find_available_port server = TCPServer.new('127.0.0.1', 0) server.addr[1] ensure server.close if server end def boot # Use WEBrick since it's part of the ruby standard library and is available on all ruby interpreters. options = { :Port => port, :ShutdownSocketWithoutClose => true } options.merge!(:AccessLog => [], :Logger => WEBrick::BasicLog.new(StringIO.new)) unless ENV['VERBOSE_SERVER'] Rack::Handler::WEBrick.run(Identify.new(@rack_app), options) end def booted? res = ::Net::HTTP.get_response("localhost", '/__identify__', port) if res.is_a?(::Net::HTTPSuccess) or res.is_a?(::Net::HTTPRedirection) return res.body == READY_MESSAGE end rescue Errno::ECONNREFUSED, Errno::EBADF return false end def concurrently # JRuby doesn't support forking. # Rubinius does, but there's a weird issue with the booted? check not working, # so we're just using a thread for now. Thread.new { yield } end def wait_until(timeout, error_message, &block) start_time = Time.now while true return if yield raise TimeoutError.new(error_message) if (Time.now - start_time) > timeout sleep(0.01) end end end end vcr-5.0.0/spec/support/vcr_stub_helpers.rb000066400000000000000000000012271347305653000206620ustar00rootroot00000000000000module VCRStubHelpers def interactions_from(file) hashes = YAML.load_file(File.join(VCR::SPEC_ROOT, 'fixtures', file))['http_interactions'] hashes.map { |h| VCR::HTTPInteraction.from_hash(h) } end def stub_requests(*args) allow(VCR).to receive(:http_interactions).and_return(VCR::Cassette::HTTPInteractionList.new(*args)) end def http_interaction(url, response_body = "FOO!", status_code = 200) request = VCR::Request.new(:get, request_url) response_status = VCR::ResponseStatus.new(status_code) response = VCR::Response.new(response_status, nil, response_body, '1.1') VCR::HTTPInteraction.new(request, response) end end vcr-5.0.0/vcr.gemspec000066400000000000000000000035541347305653000144620ustar00rootroot00000000000000#!/usr/bin/env ruby # coding: utf-8 lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "vcr/version" Gem::Specification.new do |spec| spec.name = "vcr" spec.version = VCR.version spec.authors = ["Myron Marston"] spec.email = ["myron.marston@gmail.com"] spec.summary = %q{Record your test suite's HTTP interactions and replay them during future test runs for fast, deterministic, accurate tests.} spec.description = spec.summary spec.homepage = "https://relishapp.com/vcr/vcr/docs" spec.license = "MIT" spec.files = Dir[File.join("lib", "**", "*")] spec.executables = Dir[File.join("bin", "**", "*")].map! { |f| f.gsub(/bin\//, "") } spec.require_paths = ["lib"] spec.required_ruby_version = ">= 1.9.3" spec.add_development_dependency "bundler", "~> 2.0" spec.add_development_dependency "rspec", "~> 3.0" spec.add_development_dependency "test-unit", "~> 3.1.4" spec.add_development_dependency "rake", "~> 10.1" spec.add_development_dependency "pry", "~> 0.9" spec.add_development_dependency "pry-doc", "~> 0.6" spec.add_development_dependency "codeclimate-test-reporter", "~> 0.4" spec.add_development_dependency "yard" spec.add_development_dependency "rack" spec.add_development_dependency "webmock" spec.add_development_dependency "cucumber", "~> 2.0.2" spec.add_development_dependency "aruba", "~> 0.5.3" spec.add_development_dependency "faraday", "~> 0.11.0" spec.add_development_dependency "httpclient" spec.add_development_dependency "excon", "0.62.0" spec.add_development_dependency "timecop" spec.add_development_dependency "multi_json" spec.add_development_dependency "json" spec.add_development_dependency "relish" spec.add_development_dependency "mime-types" spec.add_development_dependency "sinatra" end