lib/vcr/cassette/migrator.rb

Summary

Maintainability
A
25 mins
Test Coverage
require '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