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
@@ -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
+