vcr 2.0.0.beta1 → 2.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
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