vcr 2.0.0.beta1 → 2.0.0.beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. data/.gitignore +1 -0
  2. data/.travis.yml +3 -0
  3. data/CHANGELOG.md +37 -2
  4. data/Gemfile +2 -2
  5. data/README.md +10 -1
  6. data/Rakefile +43 -7
  7. data/Upgrade.md +45 -0
  8. data/features/.nav +1 -0
  9. data/features/cassettes/automatic_re_recording.feature +19 -17
  10. data/features/cassettes/dynamic_erb.feature +32 -28
  11. data/features/cassettes/exclusive.feature +28 -24
  12. data/features/cassettes/format.feature +213 -31
  13. data/features/cassettes/update_content_length_header.feature +20 -18
  14. data/features/configuration/filter_sensitive_data.feature +4 -4
  15. data/features/configuration/hooks.feature +27 -23
  16. data/features/http_libraries/em_http_request.feature +79 -75
  17. data/features/record_modes/all.feature +14 -14
  18. data/features/record_modes/new_episodes.feature +15 -15
  19. data/features/record_modes/none.feature +15 -15
  20. data/features/record_modes/once.feature +15 -15
  21. data/features/request_matching/body.feature +25 -23
  22. data/features/request_matching/custom_matcher.feature +25 -23
  23. data/features/request_matching/headers.feature +32 -36
  24. data/features/request_matching/host.feature +27 -25
  25. data/features/request_matching/identical_request_sequence.feature +27 -25
  26. data/features/request_matching/method.feature +27 -25
  27. data/features/request_matching/path.feature +27 -25
  28. data/features/request_matching/playback_repeats.feature +27 -25
  29. data/features/request_matching/uri.feature +27 -25
  30. data/features/request_matching/uri_without_param.feature +28 -26
  31. data/features/step_definitions/cli_steps.rb +71 -17
  32. data/features/support/env.rb +3 -1
  33. data/features/support/http_lib_filters.rb +6 -3
  34. data/features/support/vcr_cucumber_helpers.rb +4 -2
  35. data/lib/vcr.rb +6 -2
  36. data/lib/vcr/cassette.rb +75 -51
  37. data/lib/vcr/cassette/migrator.rb +111 -0
  38. data/lib/vcr/cassette/serializers.rb +35 -0
  39. data/lib/vcr/cassette/serializers/json.rb +23 -0
  40. data/lib/vcr/cassette/serializers/psych.rb +24 -0
  41. data/lib/vcr/cassette/serializers/syck.rb +35 -0
  42. data/lib/vcr/cassette/serializers/yaml.rb +24 -0
  43. data/lib/vcr/configuration.rb +6 -1
  44. data/lib/vcr/errors.rb +1 -1
  45. data/lib/vcr/library_hooks/excon.rb +1 -7
  46. data/lib/vcr/library_hooks/typhoeus.rb +6 -22
  47. data/lib/vcr/library_hooks/webmock.rb +1 -1
  48. data/lib/vcr/middleware/faraday.rb +1 -1
  49. data/lib/vcr/request_matcher_registry.rb +43 -30
  50. data/lib/vcr/structs.rb +209 -0
  51. data/lib/vcr/tasks/vcr.rake +9 -0
  52. data/lib/vcr/version.rb +1 -1
  53. data/spec/fixtures/cassette_spec/1_x_cassette.yml +110 -0
  54. data/spec/fixtures/cassette_spec/example.yml +79 -78
  55. data/spec/fixtures/cassette_spec/with_localhost_requests.yml +79 -77
  56. data/spec/fixtures/fake_example.com_responses.yml +78 -76
  57. data/spec/fixtures/match_requests_on.yml +147 -145
  58. data/spec/monkey_patches.rb +5 -5
  59. data/spec/support/http_library_adapters.rb +48 -0
  60. data/spec/support/shared_example_groups/hook_into_http_library.rb +53 -20
  61. data/spec/support/sinatra_app.rb +12 -0
  62. data/spec/vcr/cassette/http_interaction_list_spec.rb +1 -1
  63. data/spec/vcr/cassette/migrator_spec.rb +183 -0
  64. data/spec/vcr/cassette/serializers_spec.rb +122 -0
  65. data/spec/vcr/cassette_spec.rb +147 -83
  66. data/spec/vcr/configuration_spec.rb +11 -1
  67. data/spec/vcr/library_hooks/typhoeus_spec.rb +3 -3
  68. data/spec/vcr/library_hooks/webmock_spec.rb +7 -1
  69. data/spec/vcr/request_ignorer_spec.rb +1 -1
  70. data/spec/vcr/request_matcher_registry_spec.rb +46 -4
  71. data/spec/vcr/structs_spec.rb +309 -0
  72. data/spec/vcr_spec.rb +7 -0
  73. data/vcr.gemspec +9 -12
  74. metadata +75 -61
  75. data/lib/vcr/structs/http_interaction.rb +0 -58
  76. data/lib/vcr/structs/normalizers/body.rb +0 -24
  77. data/lib/vcr/structs/normalizers/header.rb +0 -64
  78. data/lib/vcr/structs/normalizers/status_message.rb +0 -17
  79. data/lib/vcr/structs/normalizers/uri.rb +0 -34
  80. data/lib/vcr/structs/request.rb +0 -13
  81. data/lib/vcr/structs/response.rb +0 -13
  82. data/lib/vcr/structs/response_status.rb +0 -5
  83. data/lib/vcr/util/yaml.rb +0 -11
  84. data/spec/support/shared_example_groups/normalizers.rb +0 -94
  85. data/spec/vcr/structs/http_interaction_spec.rb +0 -89
  86. data/spec/vcr/structs/request_spec.rb +0 -39
  87. data/spec/vcr/structs/response_spec.rb +0 -44
  88. data/spec/vcr/structs/response_status_spec.rb +0 -9
@@ -0,0 +1,111 @@
1
+ require 'yaml'
2
+ require 'vcr/structs'
3
+ require 'uri'
4
+
5
+ module VCR
6
+ class Cassette
7
+ class Migrator
8
+ def initialize(dir, out = $stdout)
9
+ @dir, @out = dir, out
10
+ @yaml_load_errors = yaml_load_errors
11
+ end
12
+
13
+ def migrate!
14
+ @out.puts "Migrating VCR cassettes in #{@dir}..."
15
+ Dir["#{@dir}/**/*.yml"].each do |cassette|
16
+ migrate_cassette(cassette)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def migrate_cassette(cassette)
23
+ unless http_interactions = load_yaml(cassette)
24
+ @out.puts " - Ignored #{relative_casssette_name(cassette)} since it could not be parsed as YAML (does it have some ERB?)"
25
+ return
26
+ end
27
+
28
+ unless valid_vcr_1_cassette?(http_interactions)
29
+ @out.puts " - Ignored #{relative_casssette_name(cassette)} since it does not appear to be a valid VCR 1.x cassette"
30
+ return
31
+ end
32
+
33
+ http_interactions.map! do |interaction|
34
+ interaction.recorded_at = File.mtime(cassette)
35
+ remove_unnecessary_standard_port(interaction)
36
+ denormalize_http_header_keys(interaction.request)
37
+ denormalize_http_header_keys(interaction.response)
38
+ normalize_body(interaction.request)
39
+ normalize_body(interaction.response)
40
+ interaction.to_hash
41
+ end
42
+
43
+ hash = {
44
+ "http_interactions" => http_interactions,
45
+ "recorded_with" => "VCR 1.11.3" # assume the last 1.x release
46
+ }
47
+
48
+ def hash.each
49
+ yield 'http_interactions', self['http_interactions']
50
+ yield 'recorded_with', self['recorded_with']
51
+ end
52
+
53
+ File.open(cassette, 'w') { |f| f.write ::YAML.dump(hash) }
54
+ @out.puts " - Migrated #{relative_casssette_name(cassette)}"
55
+ end
56
+
57
+ def load_yaml(cassette)
58
+ ::YAML.load_file(cassette)
59
+ rescue *@yaml_load_errors
60
+ return nil
61
+ end
62
+
63
+ def yaml_load_errors
64
+ [ArgumentError].tap do |errors|
65
+ errors << Psych::SyntaxError if defined?(Psych::SyntaxError)
66
+ end
67
+ end
68
+
69
+ def relative_casssette_name(cassette)
70
+ cassette.gsub(%r|\A#{Regexp.escape(@dir)}/?|, '')
71
+ end
72
+
73
+ def valid_vcr_1_cassette?(content)
74
+ content.is_a?(Array) &&
75
+ content.map(&:class).uniq == [HTTPInteraction]
76
+ end
77
+
78
+ def remove_unnecessary_standard_port(interaction)
79
+ uri = URI(interaction.request.uri)
80
+ if uri.scheme == 'http' && uri.port == 80 ||
81
+ uri.scheme == 'https' && uri.port == 443
82
+ uri.port = nil
83
+ interaction.request.uri = uri.to_s
84
+ end
85
+ rescue URI::InvalidURIError
86
+ # ignore this URI.
87
+ # This can occur when the user uses the filter_sensitive_data option
88
+ # to put a substitution string in their URI
89
+ end
90
+
91
+ def denormalize_http_header_keys(object)
92
+ object.headers = {}.tap do |denormalized|
93
+ object.headers.each do |k, v|
94
+ denormalized[denormalize_header_key(k)] = v
95
+ end if object.headers
96
+ end
97
+ end
98
+
99
+ def denormalize_header_key(key)
100
+ key.split('-'). # 'user-agent' => %w(user agent)
101
+ each { |w| w.capitalize! }. # => %w(User Agent)
102
+ join('-')
103
+ end
104
+
105
+ def normalize_body(object)
106
+ object.body = '' if object.body.nil?
107
+ end
108
+ end
109
+ end
110
+ end
111
+
@@ -0,0 +1,35 @@
1
+ module VCR
2
+ class Cassette
3
+ class Serializers
4
+ autoload :YAML, 'vcr/cassette/serializers/yaml'
5
+ autoload :Syck, 'vcr/cassette/serializers/syck'
6
+ autoload :Psych, 'vcr/cassette/serializers/psych'
7
+ autoload :JSON, 'vcr/cassette/serializers/json'
8
+
9
+ def initialize
10
+ @serializers = {}
11
+ end
12
+
13
+ def [](name)
14
+ @serializers.fetch(name) do |_|
15
+ @serializers[name] = case name
16
+ when :yaml then YAML
17
+ when :syck then Syck
18
+ when :psych then Psych
19
+ when :json then JSON
20
+ else raise ArgumentError.new("The requested VCR cassette serializer (#{name.inspect}) is not registered.")
21
+ end
22
+ end
23
+ end
24
+
25
+ def []=(name, value)
26
+ if @serializers.has_key?(name)
27
+ warn "WARNING: There is already a VCR cassette serializer registered for #{name.inspect}. Overriding it."
28
+ end
29
+
30
+ @serializers[name] = value
31
+ end
32
+ end
33
+ end
34
+ end
35
+
@@ -0,0 +1,23 @@
1
+ require 'multi_json'
2
+
3
+ module VCR
4
+ class Cassette
5
+ class Serializers
6
+ module JSON
7
+ extend self
8
+
9
+ def file_extension
10
+ "json"
11
+ end
12
+
13
+ def serialize(hash)
14
+ MultiJson.encode(hash)
15
+ end
16
+
17
+ def deserialize(string)
18
+ MultiJson.decode(string)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ require 'psych'
2
+
3
+ module VCR
4
+ class Cassette
5
+ class Serializers
6
+ module Psych
7
+ extend self
8
+
9
+ def file_extension
10
+ "yml"
11
+ end
12
+
13
+ def serialize(hash)
14
+ ::Psych.dump(hash)
15
+ end
16
+
17
+ def deserialize(string)
18
+ ::Psych.load(string)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,35 @@
1
+ require 'yaml'
2
+
3
+ module VCR
4
+ class Cassette
5
+ class Serializers
6
+ module Syck
7
+ extend self
8
+
9
+ def file_extension
10
+ "yml"
11
+ end
12
+
13
+ def serialize(hash)
14
+ using_syck { ::YAML.dump(hash) }
15
+ end
16
+
17
+ def deserialize(string)
18
+ using_syck { ::YAML.load(string) }
19
+ end
20
+
21
+ def using_syck
22
+ return yield unless defined?(::YAML::ENGINE)
23
+ original_engine = ::YAML::ENGINE.yamler
24
+ ::YAML::ENGINE.yamler = 'syck'
25
+
26
+ begin
27
+ yield
28
+ ensure
29
+ ::YAML::ENGINE.yamler = original_engine
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,24 @@
1
+ require 'yaml'
2
+
3
+ module VCR
4
+ class Cassette
5
+ class Serializers
6
+ module YAML
7
+ extend self
8
+
9
+ def file_extension
10
+ "yml"
11
+ end
12
+
13
+ def serialize(hash)
14
+ ::YAML.dump(hash)
15
+ end
16
+
17
+ def deserialize(string)
18
+ ::YAML.load(string)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+
@@ -14,7 +14,8 @@ module VCR
14
14
  @allow_http_connections_when_no_cassette = nil
15
15
  @default_cassette_options = {
16
16
  :record => :once,
17
- :match_requests_on => RequestMatcherRegistry::DEFAULT_MATCHERS
17
+ :match_requests_on => RequestMatcherRegistry::DEFAULT_MATCHERS,
18
+ :serialize_with => :yaml
18
19
  }
19
20
  end
20
21
 
@@ -62,6 +63,10 @@ module VCR
62
63
  end
63
64
  end
64
65
 
66
+ def cassette_serializers
67
+ VCR.cassette_serializers
68
+ end
69
+
65
70
  private
66
71
 
67
72
  def load_library_hook(hook)
data/lib/vcr/errors.rb CHANGED
@@ -6,7 +6,7 @@ module VCR
6
6
  class MissingERBVariableError < Error; end
7
7
  class LibraryVersionTooLowError < Error; end
8
8
  class UnregisteredMatcherError < Error; end
9
-
9
+ class InvalidCassetteFormatError < Error; end
10
10
 
11
11
  class HTTPConnectionNotAllowedError < Error
12
12
  def initialize(request)
@@ -112,17 +112,11 @@ module VCR
112
112
  normalized = {}
113
113
  headers.each do |k, v|
114
114
  v = v.join(', ') if v.respond_to?(:join)
115
- normalized[normalize_header_key(k)] = v
115
+ normalized[k] = v
116
116
  end
117
117
  normalized
118
118
  end
119
119
 
120
- def normalize_header_key(key)
121
- key.split('-'). # 'user-agent' => %w(user agent)
122
- each { |w| w.capitalize! }. # => %w(User Agent)
123
- join('-')
124
- end
125
-
126
120
  ::Excon.stub({}) do |params|
127
121
  self.new(params).handle
128
122
  end
@@ -2,7 +2,7 @@ require 'vcr/util/version_checker'
2
2
  require 'vcr/request_handler'
3
3
  require 'typhoeus'
4
4
 
5
- VCR::VersionChecker.new('Typhoeus', Typhoeus::VERSION, '0.2.1', '0.2').check_version!
5
+ VCR::VersionChecker.new('Typhoeus', Typhoeus::VERSION, '0.3.2', '0.3').check_version!
6
6
 
7
7
  module VCR
8
8
  class LibraryHooks
@@ -35,16 +35,12 @@ module VCR
35
35
 
36
36
  private
37
37
 
38
- def on_stubbed_request
39
- hydra_mock
40
- end
41
-
42
38
  def vcr_request
43
39
  @vcr_request ||= vcr_request_from(request)
44
40
  end
45
41
 
46
- def typhoeus_response
47
- @typhoeus_response ||= ::Typhoeus::Response.new \
42
+ def on_stubbed_request
43
+ ::Typhoeus::Response.new \
48
44
  :http_version => stubbed_response.http_version,
49
45
  :code => stubbed_response.status.code,
50
46
  :status_message => stubbed_response.status.message,
@@ -52,12 +48,6 @@ module VCR
52
48
  :body => stubbed_response.body
53
49
  end
54
50
 
55
- def hydra_mock
56
- @hydra_mock ||= ::Typhoeus::HydraMock.new(/.*/, :any).tap do |m|
57
- m.and_return(typhoeus_response)
58
- end
59
- end
60
-
61
51
  def stubbed_response_headers
62
52
  @stubbed_response_headers ||= {}.tap do |hash|
63
53
  stubbed_response.headers.each do |key, values|
@@ -75,19 +65,13 @@ module VCR
75
65
  end
76
66
  end
77
67
 
68
+ ::Typhoeus::Hydra.register_stub_finder do |request|
69
+ VCR::LibraryHooks::Typhoeus::RequestHandler.new(request).handle
70
+ end
78
71
  end
79
72
  end
80
73
  end
81
74
 
82
- # TODO: add Typhoeus::Hydra.register_stub_finder API to Typhoeus
83
- # so we can use that instead of monkey-patching it.
84
- Typhoeus::Hydra::Stubbing::SharedMethods.class_eval do
85
- undef find_stub_from_request
86
- def find_stub_from_request(request)
87
- VCR::LibraryHooks::Typhoeus::RequestHandler.new(request).handle
88
- end
89
- end
90
-
91
75
  VCR.configuration.after_library_hooks_loaded do
92
76
  # ensure WebMock's Typhoeus adapter does not conflict with us here
93
77
  # (i.e. to double record requests or whatever).
@@ -29,7 +29,7 @@ module VCR
29
29
  VCR::ResponseStatus.new(response.status.first, response.status.last),
30
30
  response.headers,
31
31
  response.body,
32
- '1.1'
32
+ nil
33
33
  end
34
34
  end
35
35
 
@@ -45,7 +45,7 @@ module VCR
45
45
  VCR::ResponseStatus.new(response.status, nil),
46
46
  response.headers,
47
47
  response.body,
48
- '1.1'
48
+ nil
49
49
  )
50
50
  end
51
51
 
@@ -1,3 +1,5 @@
1
+ require 'vcr/errors'
2
+
1
3
  module VCR
2
4
  class RequestMatcherRegistry
3
5
  DEFAULT_MATCHERS = [:method, :uri]
@@ -8,6 +10,33 @@ module VCR
8
10
  end
9
11
  end
10
12
 
13
+ class URIWithoutParamsMatcher < Struct.new(:params_to_ignore)
14
+ def partial_uri_from(request)
15
+ URI(request.uri).tap do |uri|
16
+ break unless uri.query # ignore uris without params, e.g. "http://example.com/"
17
+
18
+ uri.query = uri.query.split('&').tap { |params|
19
+ params.map! do |p|
20
+ key, value = p.split('=')
21
+ key.gsub!(/\[\]\z/, '') # handle params like tag[]=
22
+ [key, value]
23
+ end
24
+
25
+ params.reject! { |p| params_to_ignore.include?(p.first) }
26
+ params.map! { |p| p.join('=') }
27
+ }.join('&')
28
+ end
29
+ end
30
+
31
+ def call(request_1, request_2)
32
+ partial_uri_from(request_1) == partial_uri_from(request_2)
33
+ end
34
+
35
+ def to_proc
36
+ lambda { |r1, r2| call(r1, r2) }
37
+ end
38
+ end
39
+
11
40
  def initialize
12
41
  @registry = {}
13
42
  register_built_ins
@@ -30,31 +59,19 @@ module VCR
30
59
  end
31
60
 
32
61
  def uri_without_params(*ignores)
33
- ignores = ignores.map { |i| i.to_s }
34
-
35
- lambda do |request_1, request_2|
36
- uri_1, uri_2 = [request_1, request_2].map do |r|
37
- URI(r.uri).tap do |uri|
38
- uri.query = uri.query.split('&').tap { |params|
39
- params.map! do |p|
40
- key, value = p.split('=')
41
- key.gsub!(/\[\]\z/, '') # handle params like tag[]=
42
- [key, value]
43
- end
44
-
45
- params.reject! { |p| ignores.include?(p.first) }
46
- params.map! { |p| p.join('=') }
47
- }.join('&')
48
- end
49
- end
50
-
51
- uri_1 == uri_2
52
- end
62
+ uri_without_param_matchers[ignores]
53
63
  end
54
64
  alias uri_without_param uri_without_params
55
65
 
56
66
  private
57
67
 
68
+ def uri_without_param_matchers
69
+ @uri_without_param_matchers ||= Hash.new do |hash, params|
70
+ params = params.map(&:to_s)
71
+ hash[params] = URIWithoutParamsMatcher.new(params)
72
+ end
73
+ end
74
+
58
75
  def raise_unregistered_matcher_error(name)
59
76
  raise Errors::UnregisteredMatcherError.new \
60
77
  "There is no matcher registered for #{name.inspect}. " +
@@ -63,22 +80,18 @@ module VCR
63
80
 
64
81
  def register_built_ins
65
82
  register(:method) { |r1, r2| r1.method == r2.method }
66
- register(:uri) { |r1, r2| normalize_uri(r1.uri) == normalize_uri(r2.uri) }
83
+ register(:uri) { |r1, r2| without_standard_port(r1.uri) == without_standard_port(r2.uri) }
67
84
  register(:host) { |r1, r2| URI(r1.uri).host == URI(r2.uri).host }
68
85
  register(:path) { |r1, r2| URI(r1.uri).path == URI(r2.uri).path }
69
86
  register(:body) { |r1, r2| r1.body == r2.body }
70
87
  register(:headers) { |r1, r2| r1.headers == r2.headers }
71
88
  end
72
89
 
73
- def normalize_uri(uri)
74
- # TODO: find a better, less-hacky way to do this.
75
- if defined?(::WebMock)
76
- ::WebMock::Util::URI.normalize_uri(uri).to_s
77
- elsif defined?(VCR::Middleware::Faraday)
78
- # Faraday normalizes URIs by replacing '+' with '%20'
79
- uri.gsub('+', '%20')
80
- else
81
- uri
90
+ def without_standard_port(uri)
91
+ URI(uri).tap do |u|
92
+ if [['http', 80], ['https', 443]].include?([u.scheme, u.port])
93
+ u.port = nil
94
+ end
82
95
  end
83
96
  end
84
97
  end