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
@@ -18,7 +18,8 @@ describe VCR::Configuration do
18
18
  it 'has a hash with some defaults' do
19
19
  subject.default_cassette_options.should eq({
20
20
  :match_requests_on => VCR::RequestMatcherRegistry::DEFAULT_MATCHERS,
21
- :record => :once
21
+ :record => :once,
22
+ :serialize_with => :yaml
22
23
  })
23
24
  end
24
25
 
@@ -140,4 +141,13 @@ describe VCR::Configuration do
140
141
  yielded_interaction.should equal(interaction)
141
142
  end
142
143
  end
144
+
145
+ describe "#cassette_serializers" do
146
+ let(:custom_serializer) { stub }
147
+ it 'allows a custom serializer to be registered' do
148
+ expect { subject.cassette_serializers[:custom] }.to raise_error(ArgumentError)
149
+ subject.cassette_serializers[:custom] = custom_serializer
150
+ subject.cassette_serializers[:custom].should be(custom_serializer)
151
+ end
152
+ end
143
153
  end
@@ -4,9 +4,9 @@ describe "Typhoeus hook", :with_monkey_patches => :typhoeus do
4
4
  it_behaves_like 'a hook into an HTTP library', 'typhoeus'
5
5
 
6
6
  it_performs('version checking', 'Typhoeus',
7
- :valid => %w[ 0.2.1 0.2.99 ],
8
- :too_low => %w[ 0.1.0 0.1.31 0.2.0 ],
9
- :too_high => %w[ 0.3.0 1.0.0 ]
7
+ :valid => %w[ 0.3.2 0.3.10 ],
8
+ :too_low => %w[ 0.2.0 0.2.31 0.3.1 ],
9
+ :too_high => %w[ 0.4.0 1.0.0 ]
10
10
  ) do
11
11
  before(:each) { @orig_version = Typhoeus::VERSION }
12
12
  after(:each) { Typhoeus::VERSION = @orig_version }
@@ -2,7 +2,13 @@ require 'spec_helper'
2
2
 
3
3
  describe "WebMock hook", :with_monkey_patches => :webmock do
4
4
  %w[net/http patron httpclient em-http-request curb typhoeus].each do |lib|
5
- it_behaves_like 'a hook into an HTTP library', lib
5
+ it_behaves_like 'a hook into an HTTP library', lib do
6
+ if lib == 'net/http'
7
+ def normalize_request_headers(headers)
8
+ headers.merge(DEFAULT_REQUEST_HEADERS)
9
+ end
10
+ end
11
+ end
6
12
  end
7
13
 
8
14
  it_performs('version checking', 'WebMock',
@@ -1,4 +1,4 @@
1
- require 'vcr/structs/http_interaction'
1
+ require 'vcr/structs'
2
2
  require 'vcr/request_ignorer'
3
3
 
4
4
  module VCR
@@ -1,5 +1,5 @@
1
1
  require 'vcr/request_matcher_registry'
2
- require 'vcr/structs/http_interaction'
2
+ require 'vcr/structs'
3
3
  require 'uri'
4
4
 
5
5
  module VCR
@@ -41,7 +41,7 @@ module VCR
41
41
  end
42
42
  end
43
43
 
44
- describe "#for" do
44
+ describe "#[]" do
45
45
  it 'returns a previously registered matcher' do
46
46
  matcher = lambda { }
47
47
  subject.register(:my_matcher, &matcher)
@@ -51,7 +51,7 @@ module VCR
51
51
  it 'raises an ArgumentError when no matcher has been registered for the given name' do
52
52
  expect {
53
53
  subject[:some_unregistered_matcher]
54
- }.to raise_error(UnregisteredMatcherError)
54
+ }.to raise_error(VCR::Errors::UnregisteredMatcherError)
55
55
  end
56
56
 
57
57
  it 'returns an object that calls the named block when #matches? is called on it' do
@@ -70,6 +70,15 @@ module VCR
70
70
 
71
71
  [:uri_without_param, :uri_without_params].each do |meth|
72
72
  describe "##{meth}" do
73
+ it 'returns a matcher that can be registered for later use' do
74
+ matcher = subject.send(meth, :foo)
75
+ subject.register(:uri_without_foo, &matcher)
76
+ subject[:uri_without_foo].matches?(
77
+ request_with(:uri => 'http://example.com/search?foo=123'),
78
+ request_with(:uri => 'http://example.com/search?foo=123')
79
+ ).should be_true
80
+ end
81
+
73
82
  it 'matches two requests with URIs that are identical' do
74
83
  subject[subject.send(meth, :foo)].matches?(
75
84
  request_with(:uri => 'http://example.com/search?foo=123'),
@@ -77,7 +86,7 @@ module VCR
77
86
  ).should be_true
78
87
  end
79
88
 
80
- it 'does not matches two requests with different path parts' do
89
+ it 'does not match two requests with different path parts' do
81
90
  subject[subject.send(meth, :foo)].matches?(
82
91
  request_with(:uri => 'http://example.com/search?foo=123'),
83
92
  request_with(:uri => 'http://example.com/find?foo=123')
@@ -118,6 +127,13 @@ module VCR
118
127
  request_with(:uri => 'http://example.com/search?foo=124&baz=9&bar=q')
119
128
  ).should be_true
120
129
  end
130
+
131
+ it 'matches two requests with URIs that have no params' do
132
+ subject[subject.send(meth, :foo, :bar)].matches?(
133
+ request_with(:uri => 'http://example.com/search'),
134
+ request_with(:uri => 'http://example.com/search')
135
+ ).should be_true
136
+ end
121
137
  end
122
138
  end
123
139
 
@@ -152,6 +168,32 @@ module VCR
152
168
  request_with(:uri => 'http://foo2.com/bar?baz=7')
153
169
  ).should be_false
154
170
  end
171
+
172
+ it 'does not consider the standard HTTP port' do
173
+ subject[:uri].matches?(
174
+ request_with(:uri => 'http://foo.com:80/bar?baz=7'),
175
+ request_with(:uri => 'http://foo.com/bar?baz=7')
176
+ ).should be_true
177
+ end
178
+
179
+ it 'does not consider the standard HTTPS port' do
180
+ subject[:uri].matches?(
181
+ request_with(:uri => 'https://foo.com/bar?baz=7'),
182
+ request_with(:uri => 'https://foo.com:443/bar?baz=7')
183
+ ).should be_true
184
+ end
185
+
186
+ it 'considers non-standard ports' do
187
+ subject[:uri].matches?(
188
+ request_with(:uri => 'http://foo.com:79/bar?baz=7'),
189
+ request_with(:uri => 'http://foo.com:78/bar?baz=7')
190
+ ).should be_false
191
+
192
+ subject[:uri].matches?(
193
+ request_with(:uri => 'https://foo.com:442/bar?baz=7'),
194
+ request_with(:uri => 'https://foo.com:441/bar?baz=7')
195
+ ).should be_false
196
+ end
155
197
  end
156
198
 
157
199
  describe ":host" do
@@ -0,0 +1,309 @@
1
+ require 'yaml'
2
+ require 'vcr/structs'
3
+
4
+ shared_examples_for "a header normalizer" do
5
+ let(:instance) do
6
+ with_headers('Some_Header' => 'value1', 'aNother' => ['a', 'b'], 'third' => [], 'fourth' => nil)
7
+ end
8
+
9
+ it 'ensures header keys are serialized to yaml as raw strings' do
10
+ key = 'my-key'
11
+ key.instance_variable_set(:@foo, 7)
12
+ instance = with_headers(key => ['value1'])
13
+ YAML.dump(instance.headers).should eq(YAML.dump('my-key' => ['value1']))
14
+ end
15
+
16
+ it 'ensures header values are serialized to yaml as raw strings' do
17
+ value = 'my-value'
18
+ value.instance_variable_set(:@foo, 7)
19
+ instance = with_headers('my-key' => [value])
20
+ YAML.dump(instance.headers).should eq(YAML.dump('my-key' => ['my-value']))
21
+ end
22
+
23
+ it 'handles nested arrays' do
24
+ accept_encoding = [["gzip", "1.0"], ["deflate", "1.0"], ["sdch", "1.0"]]
25
+ instance = with_headers('accept-encoding' => accept_encoding)
26
+ instance.headers['accept-encoding'].should eq(accept_encoding)
27
+ end
28
+
29
+ it 'handles nested arrays with floats' do
30
+ accept_encoding = [["gzip", 1.0], ["deflate", 1.0], ["sdch", 1.0]]
31
+ instance = with_headers('accept-encoding' => accept_encoding)
32
+ instance.headers['accept-encoding'].should eq(accept_encoding)
33
+ end
34
+ end
35
+
36
+ shared_examples_for "a body normalizer" do
37
+ it "ensures the body is serialized to yaml as a raw string" do
38
+ body = "My String"
39
+ body.instance_variable_set(:@foo, 7)
40
+ YAML.dump(instance(body).body).should eq(YAML.dump("My String"))
41
+ end
42
+
43
+ it 'converts nil to a blank string' do
44
+ instance(nil).body.should eq("")
45
+ end
46
+ end
47
+
48
+ module VCR
49
+ describe HTTPInteraction do
50
+ %w( uri method ).each do |attr|
51
+ it "delegates :#{attr} to the request" do
52
+ sig = mock('request')
53
+ sig.should_receive(attr).and_return(:the_value)
54
+ instance = described_class.new(sig, nil)
55
+ instance.send(attr).should eq(:the_value)
56
+ end
57
+ end
58
+
59
+ describe '#ignored?' do
60
+ it 'returns false by default' do
61
+ should_not be_ignored
62
+ end
63
+
64
+ it 'returns true when #ignore! has been called' do
65
+ subject.ignore!
66
+ should be_ignored
67
+ end
68
+ end
69
+
70
+ describe "#recorded_at" do
71
+ let(:now) { Time.now }
72
+
73
+ it 'is initialized to the current time' do
74
+ Time.stub(:now => now)
75
+ VCR::HTTPInteraction.new.recorded_at.should eq(now)
76
+ end
77
+ end
78
+
79
+ let(:status) { ResponseStatus.new(200, "OK") }
80
+ let(:response) { Response.new(status, { "foo" => ["bar"] }, "res body", "1.1") }
81
+ let(:request) { Request.new(:get, "http://foo.com/", "req body", { "bar" => ["foo"] }) }
82
+ let(:recorded_at) { Time.utc(2011, 5, 4, 12, 30) }
83
+ let(:interaction) { HTTPInteraction.new(request, response, recorded_at) }
84
+
85
+ describe ".from_hash" do
86
+ let(:hash) do
87
+ {
88
+ 'request' => {
89
+ 'method' => 'get',
90
+ 'uri' => 'http://foo.com/',
91
+ 'body' => 'req body',
92
+ 'headers' => { "bar" => ["foo"] }
93
+ },
94
+ 'response' => {
95
+ 'status' => {
96
+ 'code' => 200,
97
+ 'message' => 'OK'
98
+ },
99
+ 'headers' => { "foo" => ["bar"] },
100
+ 'body' => 'res body',
101
+ 'http_version' => '1.1'
102
+ },
103
+ 'recorded_at' => "Wed, 04 May 2011 12:30:00 GMT"
104
+ }
105
+ end
106
+
107
+ it 'constructs an HTTP interaction from the given hash' do
108
+ HTTPInteraction.from_hash(hash).should eq(interaction)
109
+ end
110
+
111
+ it 'initializes the recorded_at timestamp from the hash' do
112
+ HTTPInteraction.from_hash(hash).recorded_at.should eq(recorded_at)
113
+ end
114
+
115
+ it 'uses a blank request when the hash lacks one' do
116
+ hash.delete('request')
117
+ i = HTTPInteraction.from_hash(hash)
118
+ i.request.should eq(Request.new)
119
+ end
120
+
121
+ it 'uses a blank response when the hash lacks one' do
122
+ hash.delete('response')
123
+ i = HTTPInteraction.from_hash(hash)
124
+ i.response.should eq(Response.new(ResponseStatus.new))
125
+ end
126
+ end
127
+
128
+ describe "#to_hash" do
129
+ let(:hash) { interaction.to_hash }
130
+
131
+ it 'returns a nested hash containing all of the pertinent details' do
132
+ hash.keys.should =~ %w[ request response recorded_at ]
133
+
134
+ hash['recorded_at'].should eq(interaction.recorded_at.httpdate)
135
+
136
+ hash['request'].should eq({
137
+ 'method' => 'get',
138
+ 'uri' => 'http://foo.com/',
139
+ 'body' => 'req body',
140
+ 'headers' => { "bar" => ["foo"] }
141
+ })
142
+
143
+ hash['response'].should eq({
144
+ 'status' => {
145
+ 'code' => 200,
146
+ 'message' => 'OK'
147
+ },
148
+ 'headers' => { "foo" => ["bar"] },
149
+ 'body' => 'res body',
150
+ 'http_version' => '1.1'
151
+ })
152
+ end
153
+
154
+ def assert_yielded_keys(hash, *keys)
155
+ yielded_keys = []
156
+ hash.each { |k, v| yielded_keys << k }
157
+ yielded_keys.should eq(keys)
158
+ end
159
+
160
+ it 'yields the entries in the expected order so the hash can be serialized in that order' do
161
+ assert_yielded_keys hash, 'request', 'response', 'recorded_at'
162
+ assert_yielded_keys hash['request'], 'method', 'uri', 'body', 'headers'
163
+ assert_yielded_keys hash['response'], 'status', 'headers', 'body', 'http_version'
164
+ assert_yielded_keys hash['response']['status'], 'code', 'message'
165
+ end
166
+ end
167
+
168
+ describe '#filter!' do
169
+ let(:response_status) { VCR::ResponseStatus.new(200, "OK foo") }
170
+ let(:body) { "The body foo this is (foo-Foo)" }
171
+ let(:headers) do {
172
+ 'x-http-foo' => ['bar23', '23foo'],
173
+ 'x-http-bar' => ['foo23', '18']
174
+ } end
175
+
176
+ let(:response) do
177
+ VCR::Response.new(
178
+ response_status,
179
+ headers.dup,
180
+ body.dup,
181
+ '1.1'
182
+ )
183
+ end
184
+
185
+ let(:request) do
186
+ VCR::Request.new(
187
+ :get,
188
+ 'http://example-foo.com:80/foo/',
189
+ body.dup,
190
+ headers.dup
191
+ )
192
+ end
193
+
194
+ let(:interaction) { VCR::HTTPInteraction.new(request, response) }
195
+
196
+ subject { interaction.filter!('foo', 'AAA') }
197
+
198
+ it 'does nothing when given a blank argument' do
199
+ expect {
200
+ interaction.filter!(nil, 'AAA')
201
+ interaction.filter!('foo', nil)
202
+ interaction.filter!("", 'AAA')
203
+ interaction.filter!('foo', "")
204
+ }.not_to change { interaction }
205
+ end
206
+
207
+ [:request, :response].each do |part|
208
+ it "replaces the sensitive text in the #{part} header keys and values" do
209
+ subject.send(part).headers.should eq({
210
+ 'x-http-AAA' => ['bar23', '23AAA'],
211
+ 'x-http-bar' => ['AAA23', '18']
212
+ })
213
+ end
214
+
215
+ it "replaces the sensitive text in the #{part} body" do
216
+ subject.send(part).body.should eq("The body AAA this is (AAA-Foo)")
217
+ end
218
+ end
219
+
220
+ it 'replaces the sensitive text in the response status' do
221
+ subject.response.status.message.should eq('OK AAA')
222
+ end
223
+
224
+ it 'replaces sensitive text in the request URI' do
225
+ subject.request.uri.should eq('http://example-AAA.com:80/AAA/')
226
+ end
227
+ end
228
+ end
229
+
230
+ describe Request do
231
+ describe '#method' do
232
+ subject { VCR::Request.new(:get) }
233
+
234
+ context 'when given no arguments' do
235
+ it 'returns the HTTP method' do
236
+ subject.method.should eq(:get)
237
+ end
238
+ end
239
+
240
+ context 'when given an argument' do
241
+ it 'returns the method object for the named method' do
242
+ m = subject.method(:class)
243
+ m.should be_a(Method)
244
+ m.call.should eq(described_class)
245
+ end
246
+ end
247
+ end
248
+
249
+ it_behaves_like 'a header normalizer' do
250
+ def with_headers(headers)
251
+ described_class.new(:get, 'http://example.com/', nil, headers)
252
+ end
253
+ end
254
+
255
+ it_behaves_like 'a body normalizer' do
256
+ def instance(body)
257
+ described_class.new(:get, 'http://example.com/', body, {})
258
+ end
259
+ end
260
+ end
261
+
262
+ describe Response do
263
+ it_behaves_like 'a header normalizer' do
264
+ def with_headers(headers)
265
+ described_class.new(:status, headers, nil, '1.1')
266
+ end
267
+ end
268
+
269
+ it_behaves_like 'a body normalizer' do
270
+ def instance(body)
271
+ described_class.new(:status, {}, body, '1.1')
272
+ end
273
+ end
274
+
275
+ describe '#update_content_length_header' do
276
+ %w[ content-length Content-Length ].each do |header|
277
+ context "for the #{header} header" do
278
+ define_method :instance do |body, content_length|
279
+ headers = { 'content-type' => 'text' }
280
+ headers.merge!(header => content_length) if content_length
281
+ described_class.new(VCR::ResponseStatus.new, headers, body)
282
+ end
283
+
284
+ it 'does nothing when the response lacks a content_length header' do
285
+ inst = instance('the body', nil)
286
+ expect {
287
+ inst.update_content_length_header
288
+ }.not_to change { inst.headers[header] }
289
+ end
290
+
291
+ it 'sets the content_length header to the response body length when the header is present' do
292
+ inst = instance('the body', '3')
293
+ expect {
294
+ inst.update_content_length_header
295
+ }.to change { inst.headers[header] }.from(['3']).to(['8'])
296
+ end
297
+
298
+ it 'sets the content_length header to 0 if the response body is nil' do
299
+ inst = instance(nil, '3')
300
+ expect {
301
+ inst.update_content_length_header
302
+ }.to change { inst.headers[header] }.from(['3']).to(['0'])
303
+ end
304
+ end
305
+ end
306
+ end
307
+ end
308
+ end
309
+