mirror of
https://github.com/kevin1024/vcrpy.git
synced 2025-12-08 16:53:23 +00:00
Compare commits
187 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6590f2caf | ||
|
|
70abc5058c | ||
|
|
0c1f1e2479 | ||
|
|
8d90dba16c | ||
|
|
3072c56ed2 | ||
|
|
c84fb1886b | ||
|
|
3b05d499c3 | ||
|
|
8eb54c012f | ||
|
|
6d656717a1 | ||
|
|
d7f7152dbf | ||
|
|
483554ed2e | ||
|
|
83ff73792e | ||
|
|
fd30689c12 | ||
|
|
62f9c528b5 | ||
|
|
007fa851ed | ||
|
|
ffdba74299 | ||
|
|
2c895eb5e9 | ||
|
|
b671e7ab99 | ||
|
|
b36a1157e7 | ||
|
|
0c9761f7ff | ||
|
|
66c6909021 | ||
|
|
c0691a96e6 | ||
|
|
a302874c6d | ||
|
|
b43c63f284 | ||
|
|
2c33ae2664 | ||
|
|
e50f917cf4 | ||
|
|
4ab46f9643 | ||
|
|
23b5d49736 | ||
|
|
bc45a965b2 | ||
|
|
2da49884be | ||
|
|
1e8e7057f5 | ||
|
|
d1a3ab56b1 | ||
|
|
a65da12aeb | ||
|
|
1f99ede46f | ||
|
|
f479f205ad | ||
|
|
c72d19175f | ||
|
|
52c9bf04fe | ||
|
|
bd2121d34e | ||
|
|
f9f2b98427 | ||
|
|
9d5660b673 | ||
|
|
5e295e0603 | ||
|
|
2ef5f9208a | ||
|
|
0be7d6f238 | ||
|
|
3990b32892 | ||
|
|
b5cfd517cf | ||
|
|
adf127c073 | ||
|
|
78f6ce46b5 | ||
|
|
ce5d2225a6 | ||
|
|
3322234b25 | ||
|
|
5d10a38160 | ||
|
|
0b1aeac25e | ||
|
|
1190a0e62e | ||
|
|
61e3bdc402 | ||
|
|
1ff5d08c8b | ||
|
|
1d8e2dbb41 | ||
|
|
62d19e5cc1 | ||
|
|
3d2da26933 | ||
|
|
65c2797f94 | ||
|
|
998dde61ec | ||
|
|
faa83b9aba | ||
|
|
a48f621bae | ||
|
|
fbd5049d38 | ||
|
|
7b253ebc6f | ||
|
|
5d1f35973d | ||
|
|
fbb6382c12 | ||
|
|
eab10578d5 | ||
|
|
e4d1db0617 | ||
|
|
7e677f516d | ||
|
|
34ce0a35ec | ||
|
|
b6195bf41e | ||
|
|
eedafb19ee | ||
|
|
434d6325ea | ||
|
|
25c0141e27 | ||
|
|
1e995c3c9b | ||
|
|
0408bdaadb | ||
|
|
9c9612f93e | ||
|
|
a3eac1f0ec | ||
|
|
710ec6f432 | ||
|
|
9d8426e668 | ||
|
|
424c658da4 | ||
|
|
f0972628ef | ||
|
|
ee28768a31 | ||
|
|
5354ef781c | ||
|
|
750e141b9d | ||
|
|
a042cb3824 | ||
|
|
96d8782d08 | ||
|
|
f9a64e1609 | ||
|
|
2fa1aaa1f7 | ||
|
|
7fe55ad8b8 | ||
|
|
2f6db0dc0c | ||
|
|
4267828a3e | ||
|
|
4e9d5f6885 | ||
|
|
5015dbd878 | ||
|
|
9b188e986f | ||
|
|
6b060e5666 | ||
|
|
bd9fa773e8 | ||
|
|
18ec57fa73 | ||
|
|
6cca703eee | ||
|
|
edf1df9188 | ||
|
|
e0c6a8429d | ||
|
|
08d4d8913a | ||
|
|
792d665893 | ||
|
|
cd32f5114c | ||
|
|
16c6135387 | ||
|
|
5aa2fb017f | ||
|
|
e6fdc735e4 | ||
|
|
f317800cb7 | ||
|
|
4302d7753e | ||
|
|
58ac00a7f6 | ||
|
|
ab27c71a81 | ||
|
|
6273c32334 | ||
|
|
2a072e4dd3 | ||
|
|
582b8eab51 | ||
|
|
e002aab999 | ||
|
|
bc5199c893 | ||
|
|
bd2d2cea72 | ||
|
|
59f3216d44 | ||
|
|
23d1717216 | ||
|
|
1452455d0b | ||
|
|
955e532162 | ||
|
|
20ff2e9d9a | ||
|
|
544d2127d3 | ||
|
|
01901b7a4e | ||
|
|
c83134ca39 | ||
|
|
ebb180f7a5 | ||
|
|
b7cbd181f4 | ||
|
|
4a4b04e5a6 | ||
|
|
6d0a8d8ed9 | ||
|
|
985e573303 | ||
|
|
73666bcb49 | ||
|
|
f5db99f079 | ||
|
|
dedca0f6e7 | ||
|
|
0d77d2dcc6 | ||
|
|
56a9a53522 | ||
|
|
34e7760d47 | ||
|
|
32b99f0719 | ||
|
|
d187d910b9 | ||
|
|
a73da71159 | ||
|
|
2385176084 | ||
|
|
14590ae3c7 | ||
|
|
d16b20a780 | ||
|
|
46a2c25f6a | ||
|
|
6bb67567f9 | ||
|
|
e84cd6f059 | ||
|
|
c0b88c2201 | ||
|
|
f003e3e4ab | ||
|
|
d0e6f9c047 | ||
|
|
1298d6f5c7 | ||
|
|
df67dd1728 | ||
|
|
559cd902e1 | ||
|
|
c44fee1f16 | ||
|
|
8620bc3af1 | ||
|
|
84bf7b6132 | ||
|
|
2cf779d776 | ||
|
|
d4494bae50 | ||
|
|
41f5fce895 | ||
|
|
a6806f2f99 | ||
|
|
624212ef15 | ||
|
|
144d25bc66 | ||
|
|
0d08157e5d | ||
|
|
ea24854093 | ||
|
|
49929e3064 | ||
|
|
188b57a2fa | ||
|
|
b84f8e963b | ||
|
|
ea13d51677 | ||
|
|
5ba4000f77 | ||
|
|
a4844d972b | ||
|
|
c2d857c585 | ||
|
|
89403c255c | ||
|
|
d50ded68ca | ||
|
|
16fa4f851d | ||
|
|
6200493896 | ||
|
|
b0a13ba690 | ||
|
|
d33b19b5bb | ||
|
|
2275749eaa | ||
|
|
16fbe40d87 | ||
|
|
deed8cab97 | ||
|
|
cf8646d8d6 | ||
|
|
c03459e582 | ||
|
|
912452e863 | ||
|
|
ce3d7270ea | ||
|
|
39d696bc49 | ||
|
|
ce94fd72fd | ||
|
|
a66f462dcd | ||
|
|
03c22d79dd | ||
|
|
5ce67dc023 | ||
|
|
07774ae6dd |
41
.travis.yml
41
.travis.yml
@@ -1,13 +1,36 @@
|
||||
language: python
|
||||
before_install: openssl version
|
||||
env:
|
||||
- WITH_REQUESTS="True"
|
||||
- WITH_REQUESTS="False"
|
||||
global:
|
||||
- secure: AifoKzwhjV94cmcQZrdQmqRu/9rkZZvWpwBv1daeAQpLOKFPGsOm3D+x2cSw9+iCfkgDZDfqQVv1kCaFVxTll8v8jTq5SJdqEY0NmGWbj/UkNtShh609oRDsuzLxAEwtVKYjf/h8K2BRea+bl1tGkwZ2vtmYS6dxNlAijjWOfds=
|
||||
- secure: LBSEg/gMj4u4Hrpo3zs6Y/1mTpd2RtcN49mZIFgTdbJ9IhpiNPqcEt647Lz94F9Eses2x2WbNuKqZKZZReY7QLbEzU1m0nN5jlaKrjcG5NR5clNABfFFyhgc0jBikyS4abAG8jc2efeaTrFuQwdoF4sE8YiVrkiVj2X5Xoi6sBk=
|
||||
matrix:
|
||||
- WITH_LIB="requests2.2"
|
||||
- WITH_LIB="requests2.3"
|
||||
- WITH_LIB="requests1.x"
|
||||
- WITH_LIB="httplib2"
|
||||
- WITH_LIB="boto"
|
||||
matrix:
|
||||
allow_failures:
|
||||
- env: WITH_LIB="boto"
|
||||
exclude:
|
||||
- env: WITH_LIB="boto"
|
||||
python: 3.3
|
||||
- env: WITH_LIB="boto"
|
||||
python: 3.4
|
||||
- env: WITH_LIB="requests1.x"
|
||||
python: 3.4
|
||||
python:
|
||||
- 2.6
|
||||
- 2.7
|
||||
- pypy
|
||||
install:
|
||||
- pip install PyYAML pytest --use-mirrors
|
||||
- if [ $WITH_REQUESTS = "True" ] ; then pip install requests; fi
|
||||
script: python setup.py test
|
||||
- 2.6
|
||||
- 2.7
|
||||
- 3.3
|
||||
- 3.4
|
||||
- pypy
|
||||
install:
|
||||
- pip install PyYAML pytest pytest-localserver --use-mirrors
|
||||
- if [ $WITH_LIB = "requests1.x" ] ; then pip install requests==1.2.3; fi
|
||||
- if [ $WITH_LIB = "requests2.2" ] ; then pip install requests==2.2.1; fi
|
||||
- if [ $WITH_LIB = "requests2.3" ] ; then pip install requests==2.3.0; fi
|
||||
- if [ $WITH_LIB = "httplib2" ] ; then pip install httplib2; fi
|
||||
- if [ $WITH_LIB = "boto" ] ; then pip install boto; fi
|
||||
script: python setup.py test
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2012-2013 Kevin McCarthy
|
||||
Copyright (c) 2012-2014 Kevin McCarthy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
481
README.md
481
README.md
@@ -1,28 +1,35 @@
|
||||
#VCR.py
|
||||
# VCR.py
|
||||
|
||||

|
||||
|
||||
This is a Python version of [Ruby's VCR library](https://github.com/myronmarston/vcr).
|
||||
|
||||
[](http://travis-ci.org/kevin1024/vcrpy)
|
||||
[](https://waffle.io/kevin1024/vcrpy)
|
||||
|
||||
##What it does
|
||||
Simplify and speed up testing HTTP by recording all HTTP interactions and saving them to
|
||||
"cassette" files, which are yaml files containing the contents of your
|
||||
requests and responses. Then when you run your tests again, they all
|
||||
just hit the text files instead of the internet. This speeds up
|
||||
your tests and lets you work offline.
|
||||
## What it does
|
||||
Simplify and speed up testing HTTP by recording all HTTP interactions and
|
||||
saving them to "cassette" files, which are yaml files containing the contents
|
||||
of your requests and responses. Then when you run your tests again, they all
|
||||
just hit the text files instead of the internet. This speeds up your tests and
|
||||
lets you work offline.
|
||||
|
||||
If the server you are testing against ever changes its API, all you need
|
||||
to do is delete your existing cassette files, and run your tests again.
|
||||
All of the mocked responses will be updated with the new API.
|
||||
If the server you are testing against ever changes its API, all you need to do
|
||||
is delete your existing cassette files, and run your tests again. All of the
|
||||
mocked responses will be updated with the new API.
|
||||
|
||||
##Compatibility Notes
|
||||
This should work with Python 2.6 and 2.7, and [pypy](http://pypy.org).
|
||||
## Compatibility Notes
|
||||
VCR.py supports Python 2.6 and 2.7, 3.3, 3.4, and [pypy](http://pypy.org).
|
||||
|
||||
Currently I've only tested this with urllib2, urllib3, and requests. It's known to *NOT WORK* with urllib.
|
||||
The following http libraries are supported:
|
||||
|
||||
##How to use it
|
||||
* urllib2
|
||||
* http.client (python3)
|
||||
* requests (both 1.x and 2.x versions)
|
||||
* httplib2
|
||||
* boto
|
||||
|
||||
## Usage
|
||||
```python
|
||||
import vcr
|
||||
import urllib2
|
||||
@@ -32,7 +39,7 @@ with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
|
||||
assert 'Example domains' in response
|
||||
```
|
||||
|
||||
Run this test once, and VCR.py will record the http request to
|
||||
Run this test once, and VCR.py will record the HTTP request to
|
||||
`fixtures/vcr_cassettes/synopsis.yml`. Run it again, and VCR.py will replay the
|
||||
response from iana.org when the http request is made. This test is now fast (no
|
||||
real HTTP requests are made anymore), deterministic (the test will continue to
|
||||
@@ -40,10 +47,23 @@ pass, even if you are offline, or iana.org goes down for maintenance) and
|
||||
accurate (the response will contain the same headers and body you get from a
|
||||
real request).
|
||||
|
||||
You can also use VCR.py as a decorator. The same request above would look like
|
||||
this:
|
||||
|
||||
```python
|
||||
@vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
|
||||
def test_iana():
|
||||
response = urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert 'Example domains' in response
|
||||
```
|
||||
|
||||
All of the parameters and configuration works the same for the decorator
|
||||
version.
|
||||
|
||||
## Configuration
|
||||
|
||||
If you don't like VCR's defaults, you can set options by instantiating a
|
||||
VCR class and setting the options on it.
|
||||
If you don't like VCR's defaults, you can set options by instantiating a `VCR`
|
||||
class and setting the options on it.
|
||||
|
||||
```python
|
||||
|
||||
@@ -52,6 +72,8 @@ import vcr
|
||||
my_vcr = vcr.VCR(
|
||||
serializer = 'json',
|
||||
cassette_library_dir = 'fixtures/cassettes',
|
||||
record_mode = 'once',
|
||||
match_on = ['uri', 'method'],
|
||||
)
|
||||
|
||||
with my_vcr.use_cassette('test.json'):
|
||||
@@ -61,20 +83,83 @@ with my_vcr.use_cassette('test.json'):
|
||||
Otherwise, you can override options each time you use a cassette.
|
||||
|
||||
```python
|
||||
with vcr.use_cassette('test.yml', serializer='json'):
|
||||
with vcr.use_cassette('test.yml', serializer='json', record_mode='once'):
|
||||
# your http code here
|
||||
```
|
||||
|
||||
Note: Per-cassette overrides take precedence over the global config.
|
||||
|
||||
## Request matching
|
||||
|
||||
Request matching is configurable and allows you to change which requests VCR
|
||||
considers identical. The default behavior is
|
||||
`['method', 'scheme', 'host', 'port', 'path', 'query']` which means that
|
||||
requests with both the same URL and method (ie POST or GET) are considered
|
||||
identical.
|
||||
|
||||
This can be configured by changing the `match_on` setting.
|
||||
|
||||
The following options are available :
|
||||
|
||||
* method (for example, POST or GET)
|
||||
* uri (the full URI.)
|
||||
* host (the hostname of the server receiving the request)
|
||||
* port (the port of the server receiving the request)
|
||||
* path (the path of the request)
|
||||
* query (the query string of the request)
|
||||
* body (the entire request body)
|
||||
* headers (the headers of the request)
|
||||
|
||||
Backwards compatible matchers:
|
||||
* url (the `uri` alias)
|
||||
|
||||
If these options don't work for you, you can also register your own request
|
||||
matcher. This is described in the Advanced section of this README.
|
||||
|
||||
## Record Modes
|
||||
VCR supports 4 record modes (with the same behavior as Ruby's VCR):
|
||||
|
||||
### once
|
||||
|
||||
* Replay previously recorded interactions.
|
||||
* Record new interactions if there is no cassette file.
|
||||
* Cause an error to be raised for new requests if there is a cassette file.
|
||||
|
||||
It is similar to the new_episodes record mode, but will prevent new, unexpected
|
||||
requests from being made (i.e. because the request URI changed).
|
||||
|
||||
once is the default record mode, used when you do not set one.
|
||||
|
||||
### new_episodes
|
||||
|
||||
* Record new interactions.
|
||||
* Replay previously recorded interactions. It is similar to the once record
|
||||
mode, but will always record new interactions, even if you have an existing
|
||||
recorded one that is similar, but not identical.
|
||||
|
||||
This was the default behavior in versions < 0.3.0
|
||||
|
||||
### none
|
||||
|
||||
* Replay previously recorded interactions.
|
||||
* Cause an error to be raised for any new requests. This is useful when your
|
||||
code makes potentially dangerous HTTP requests. The none record mode
|
||||
guarantees that no new HTTP requests will be made.
|
||||
|
||||
### all
|
||||
|
||||
* Record new interactions.
|
||||
* Never replay previously recorded interactions. This can be temporarily used
|
||||
to force VCR to re-record a cassette (i.e. to ensure the responses are not
|
||||
out of date) or can be used when you simply want to log all HTTP requests.
|
||||
|
||||
## Advanced Features
|
||||
|
||||
If you want, VCR.py can return information about the cassette it is
|
||||
using to record your requests and responses. This will let you record
|
||||
your requests and responses and make assertions on them, to make sure
|
||||
that your code under test is generating the expected requests and
|
||||
responses. This feature is not present in Ruby's VCR, but I think it is
|
||||
a nice addition. Here's an example:
|
||||
If you want, VCR.py can return information about the cassette it is using to
|
||||
record your requests and responses. This will let you record your requests and
|
||||
responses and make assertions on them, to make sure that your code under test
|
||||
is generating the expected requests and responses. This feature is not present
|
||||
in Ruby's VCR, but I think it is a nice addition. Here's an example:
|
||||
|
||||
```python
|
||||
import vcr
|
||||
@@ -84,45 +169,50 @@ with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml') as cass:
|
||||
response = urllib2.urlopen('http://www.zombo.com/').read()
|
||||
# cass should have 1 request inside it
|
||||
assert len(cass) == 1
|
||||
# the request url should have been http://www.zombo.com/
|
||||
assert cass.requests[0].url == 'http://www.zombo.com/'
|
||||
# the request uri should have been http://www.zombo.com/
|
||||
assert cass.requests[0].uri == 'http://www.zombo.com/'
|
||||
```
|
||||
|
||||
The Cassette object exposes the following properties which I consider
|
||||
part of the API. The fields are as follows:
|
||||
The `Cassette` object exposes the following properties which I consider part of
|
||||
the API. The fields are as follows:
|
||||
|
||||
* `requests`: A list of vcr.Request objects containing the requests made
|
||||
while this cassette was being used, ordered by the order that the
|
||||
request was made.
|
||||
* `responses`: A list of the responses made.
|
||||
* `play_count`: The number of times this cassette has had a response
|
||||
played back
|
||||
* `play_counts`: A collections.Counter showing the number of times each
|
||||
response has been played back, indexed by the request
|
||||
* `response_of(request)`: Access the response for a given request.
|
||||
* `requests`: A list of vcr.Request objects containing the requests made while
|
||||
this cassette was being used, ordered by the order that the request was made.
|
||||
* `responses`: A list of the responses made.
|
||||
* `play_count`: The number of times this cassette has had a response played
|
||||
back
|
||||
* `all_played`: A boolean indicates whether all the responses have been
|
||||
played back
|
||||
* `responses_of(request)`: Access the responses that match a given request
|
||||
|
||||
The Request object has the following properties
|
||||
The `Request` object has the following properties:
|
||||
|
||||
* `URL`: The full url of the request, including the protocol. Example: "http://www.google.com/"
|
||||
* `path`: The path of the request. For example "/" or "/home.html"
|
||||
* `uri`: The full uri of the request. Example: "https://google.com/?q=vcrpy"
|
||||
* `scheme`: The scheme used to make the request (http or https)
|
||||
* `host`: The host of the request, for example "www.google.com"
|
||||
* `port`: The port the request was made on
|
||||
* `path`: The path of the request. For example "/" or "/home.html"
|
||||
* `query`: The parsed query string of the request. Sorted list of name, value pairs.
|
||||
* `method` : The method used to make the request, for example "GET" or "POST"
|
||||
* `protocol`: The protocol used to make the request (http or https)
|
||||
* `body`: The body of the request, usually empty except for POST / PUT / etc
|
||||
|
||||
Backwards compatible properties:
|
||||
|
||||
* `url`: The `uri` alias
|
||||
* `protocol`: The `scheme` alias
|
||||
|
||||
|
||||
## Register your own serializer
|
||||
|
||||
Don't like JSON or YAML? That's OK, VCR.py can serialize to any format
|
||||
you would like. Create your own module or class instance with 2 methods:
|
||||
Don't like JSON or YAML? That's OK, VCR.py can serialize to any format you
|
||||
would like. Create your own module or class instance with 2 methods:
|
||||
|
||||
* `def deserialize(cassette_string)`
|
||||
* `def serialize(cassette_dict)`
|
||||
* `def deserialize(cassette_string)`
|
||||
* `def serialize(cassette_dict)`
|
||||
|
||||
Finally, register your class with VCR to use your
|
||||
new serializer.
|
||||
Finally, register your class with VCR to use your new serializer.
|
||||
|
||||
```
|
||||
```python
|
||||
import vcr
|
||||
|
||||
BogoSerializer(object):
|
||||
@@ -146,44 +236,275 @@ with my_vcr.use_cassette('test.bogo'):
|
||||
|
||||
```
|
||||
|
||||
##Installation
|
||||
## Register your own request matcher
|
||||
|
||||
VCR.py is a package on PyPI, so you can `pip install vcrpy` (first you may need to `brew install libyaml` [[Homebrew](http://mxcl.github.com/homebrew/)])
|
||||
Create your own method with the following signature
|
||||
|
||||
##Ruby VCR compatibility
|
||||
I'm not trying to match the format of the Ruby VCR YAML files. Cassettes generated by
|
||||
Ruby's VCR are not compatible with VCR.py.
|
||||
```python
|
||||
def my_matcher(r1, r2):
|
||||
```
|
||||
|
||||
##Known Issues
|
||||
This library is a work in progress, so the API might change on you.
|
||||
There are probably some [bugs](https://github.com/kevin1024/vcrpy/issues?labels=bug&page=1&state=open) floating around too.
|
||||
Your method receives the two requests and must return `True` if they match,
|
||||
`False` if they don't.
|
||||
|
||||
##Changelog
|
||||
* 0.2.0: Added configuration API, which lets you configure some settings
|
||||
on VCR (see the README). Also, VCR no longer saves cassettes if they
|
||||
haven't changed at all and supports JSON as well as YAML
|
||||
(thanks @sirpengi). Added amazing new skeumorphic logo, thanks @hairarrow.
|
||||
* 0.1.0: *backwards incompatible release - delete your old cassette files*:
|
||||
This release adds the ability to access the cassette to make assertions
|
||||
on it, as well as a major code refactor thanks to @dlecocq. It also
|
||||
fixes a couple longstanding bugs with redirects and HTTPS. [#3 and #4]
|
||||
* 0.0.4: If you have libyaml installed, vcrpy will use the c bindings
|
||||
instead. Speed up your tests! Thanks @dlecocq
|
||||
* 0.0.3: Add support for requests 1.2.3. Support for older versions of requests dropped (thanks @vitormazzi and @bryanhelmig)
|
||||
* 0.0.2: Add support for requests / urllib3
|
||||
* 0.0.1: Initial Release
|
||||
Finally, register your method with VCR to use your new request matcher.
|
||||
|
||||
##Similar libraries in Python
|
||||
Neither of these really implement the API I want, but I have cribbed some code
|
||||
from them.
|
||||
* https://github.com/bbangert/Dalton
|
||||
* https://github.com/storborg/replaylib
|
||||
```python
|
||||
import vcr
|
||||
|
||||
These were created after I created VCR.py but do something similar:
|
||||
def jurassic_matcher(r1, r2):
|
||||
return r1.uri == r2.uri and 'JURASSIC PARK' in r1.body
|
||||
|
||||
* https://github.com/gabrielfalcao/HTTPretty
|
||||
* https://github.com/kanzure/python-requestions
|
||||
* https://github.com/uber/cassette
|
||||
my_vcr = vcr.VCR()
|
||||
my_vcr.register_matcher('jurassic', jurassic_matcher)
|
||||
|
||||
with my_vcr.use_cassette('test.yml', match_on=['jurassic']):
|
||||
# your http here
|
||||
|
||||
# After you register, you can set the default match_on to use your new matcher
|
||||
|
||||
my_vcr.match_on = ['jurassic']
|
||||
|
||||
with my_vcr.use_cassette('test.yml'):
|
||||
# your http here
|
||||
|
||||
```
|
||||
|
||||
## Filter sensitive data from the request
|
||||
|
||||
If you are checking your cassettes into source control, and are using some form
|
||||
of authentication in your tests, you can filter out that information so it won't
|
||||
appear in your cassette files. There are a few ways to do this:
|
||||
|
||||
### Filter information from HTTP Headers
|
||||
Use the `filter_headers` configuration option with a list of headers to filter.
|
||||
|
||||
```python
|
||||
with my_vcr.use_cassette('test.yml', filter_headers=['authorization']):
|
||||
# sensitive HTTP request goes here
|
||||
```
|
||||
|
||||
### Filter information from HTTP querystring
|
||||
Use the `filter_query_parameters` configuration option with a list of query
|
||||
parameters to filter.
|
||||
|
||||
```python
|
||||
with my_vcr.use_cassette('test.yml', filter_query_parameters=['api_key']):
|
||||
requests.get('http://api.com/getdata?api_key=secretstring')
|
||||
```
|
||||
|
||||
### Custom request filtering
|
||||
|
||||
If neither of these covers your use case, you can register a callback that will
|
||||
manipulate the HTTP request before adding it to the cassette. Use the
|
||||
`before_record` configuration option to so this. Here is an
|
||||
example that will never record requests to the /login endpoint.
|
||||
|
||||
```python
|
||||
def before_record_cb(request):
|
||||
if request.path != '/login':
|
||||
return request
|
||||
|
||||
my_vcr = vcr.VCR(
|
||||
before_record = before_record_cb,
|
||||
)
|
||||
with my_vcr.use_cassette('test.yml'):
|
||||
# your http code here
|
||||
```
|
||||
|
||||
## Ignore requests
|
||||
|
||||
If you would like to completely ignore certain requests, you can do it in a
|
||||
few ways:
|
||||
|
||||
* Set the `ignore_localhost` option equal to True. This will not record any
|
||||
requests sent to (or responses from) localhost, 127.0.0.1, or 0.0.0.0.
|
||||
* Set the `ignore_hosts` configuration option to a list of hosts to ignore
|
||||
* Add a `before_record` callback that returns None for requests you want to
|
||||
ignore
|
||||
|
||||
Requests that are ignored by VCR will not be saved in a cassette, nor played
|
||||
back from a cassette. VCR will completely ignore those requests as if it
|
||||
didn't notice them at all, and they will continue to hit the server as if VCR
|
||||
were not there.
|
||||
|
||||
## Installation
|
||||
|
||||
VCR.py is a package on PyPI, so you can `pip install vcrpy` (first you may need
|
||||
to `brew install libyaml` [[Homebrew](http://mxcl.github.com/homebrew/)])
|
||||
|
||||
## Ruby VCR compatibility
|
||||
|
||||
I'm not trying to match the format of the Ruby VCR YAML files. Cassettes
|
||||
generated by Ruby's VCR are not compatible with VCR.py.
|
||||
|
||||
## Running VCR's test suite
|
||||
|
||||
The tests are all run automatically on [Travis
|
||||
CI](https://travis-ci.org/kevin1024/vcrpy), but you can also run them yourself
|
||||
using [py.test](http://pytest.org/) and [Tox](http://tox.testrun.org/). Tox
|
||||
will automatically run them in all environments VCR.py supports. The test
|
||||
suite is pretty big and slow, but you can tell tox to only run specific tests
|
||||
like this:
|
||||
|
||||
`tox -e py27requests -- -v -k "'test_status_code or test_gzip'"`
|
||||
|
||||
This will run only tests that look like `test_status_code` or `test_gzip` in
|
||||
the test suite, and only in the python 2.7 environment that has `requests`
|
||||
installed.
|
||||
|
||||
Also, in order for the boto tests to run, you will need an AWS key. Refer to
|
||||
the [boto
|
||||
documentation](http://boto.readthedocs.org/en/latest/getting_started.html) for
|
||||
how to set this up. I have marked the boto tests as optional in Travis so you
|
||||
don't have to worry about them failing if you submit a pull request.
|
||||
|
||||
|
||||
## Logging
|
||||
VCR.py has a few log messages you can turn on to help you figure out if HTTP
|
||||
requests are hitting a real server or not. You can turn them on like this:
|
||||
|
||||
```python
|
||||
import vcr
|
||||
import requests
|
||||
import logging
|
||||
|
||||
logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from vcrpy
|
||||
vcr_log = logging.getLogger("vcr")
|
||||
vcr_log.setLevel(logging.INFO)
|
||||
|
||||
with vcr.use_cassette('headers.yml'):
|
||||
requests.get('http://httpbin.org/headers')
|
||||
```
|
||||
|
||||
The first time you run this, you will see:
|
||||
|
||||
```
|
||||
INFO:vcr.stubs:<Request (GET) http://httpbin.org/headers> not in cassette, sending to real server
|
||||
```
|
||||
|
||||
The second time, you will see:
|
||||
|
||||
```
|
||||
INFO:vcr.stubs:Playing response for <Request (GET) http://httpbin.org/headers> from cassette
|
||||
```
|
||||
|
||||
If you set the loglevel to DEBUG, you will also get information about which
|
||||
matchers didn't match. This can help you with debugging custom matchers.
|
||||
|
||||
|
||||
## Upgrade
|
||||
|
||||
### New Cassette Format
|
||||
The cassette format has changed in _VCR.py 1.x_, the _VCR.py 0.x_ cassettes
|
||||
cannot be used with _VCR.py 1.x_. The easiest way to upgrade is to simply
|
||||
delete your cassettes and re-record all of them. VCR.py also provides a
|
||||
migration script that attempts to upgrade your 0.x cassettes to the new 1.x
|
||||
format. To use it, run the following command:
|
||||
|
||||
```
|
||||
python -m vcr.migration PATH
|
||||
```
|
||||
|
||||
The PATH can be either a path to the directory with cassettes or the path to a
|
||||
single cassette.
|
||||
|
||||
*Note*: Back up your cassettes files before migration.
|
||||
The migration *should* only modify cassettes using the old 0.x format.
|
||||
|
||||
## New serializer / deserializer API
|
||||
|
||||
If you made a custom serializer, you will need to update it to match the new
|
||||
API in version 1.0.x
|
||||
|
||||
* Serializers now take dicts and return strings.
|
||||
* Deserializers take strings and return dicts (instead of requests, responses
|
||||
pair)
|
||||
|
||||
|
||||
## Changelog
|
||||
* 1.0.2: Fix an issue with requests 2.3
|
||||
* 1.0.1: Fix a bug with the new ignore requests feature and the once
|
||||
record mode
|
||||
* 1.0.0: _BACKWARDS INCOMPATIBLE_: Please see the 'upgrade' section in the
|
||||
README. Take a look at the matcher section as well, you might want to
|
||||
update your `match_on` settings. Add support for filtering sensitive
|
||||
data from requests, matching query strings after the order changes and
|
||||
improving the built-in matchers, (thanks to @mshytikov), support for
|
||||
ignoring requests to certain hosts, bump supported Python3 version to
|
||||
3.4, fix some bugs with Boto support (thanks @marusich), fix error with
|
||||
URL field capitalization in README (thanks @simon-weber), added some log
|
||||
messages to help with debugging, added `all_played` property on cassette
|
||||
(thanks @mshytikov)
|
||||
* 0.7.0: VCR.py now supports Python 3! (thanks @asundg) Also I refactored
|
||||
the stub connections quite a bit to add support for the putrequest and
|
||||
putheader calls. This version also adds support for httplib2 (thanks
|
||||
@nilp0inter). I have added a couple tests for boto since it is an http
|
||||
client in its own right. Finally, this version includes a fix for a bug
|
||||
where requests wasn't being patched properly (thanks @msabramo).
|
||||
* 0.6.0: Store response headers as a list since a HTTP response can have the
|
||||
same header twice (happens with set-cookie sometimes). This has the added
|
||||
benefit of preserving the order of headers. Thanks @smallcode for the bug
|
||||
report leading to this change. I have made an effort to ensure backwards
|
||||
compatibility with the old cassettes' header storage mechanism, but if you
|
||||
want to upgrade to the new header storage, you should delete your
|
||||
cassettes and re-record them. Also this release adds better error messages
|
||||
(thanks @msabramo) and adds support for using VCR as a decorator (thanks
|
||||
@smallcode for the motivation)
|
||||
* 0.5.0: Change the `response_of` method to `responses_of` since cassettes
|
||||
can now contain more than one response for a request. Since this changes
|
||||
the API, I'm bumping the version. Also includes 2 bugfixes: a better error
|
||||
message when attempting to overwrite a cassette file, and a fix for a bug
|
||||
with requests sessions (thanks @msabramo)
|
||||
* 0.4.0: Change default request recording behavior for multiple requests. If
|
||||
you make the same request multiple times to the same URL, the response
|
||||
might be different each time (maybe the response has a timestamp in it or
|
||||
something), so this will make the same request multiple times and save them
|
||||
all. Then, when you are replaying the cassette, the responses will be
|
||||
played back in the same order in which they were received. If you were
|
||||
making multiple requests to the same URL in a cassette before version
|
||||
0.4.0, you might need to regenerate your cassette files. Also, removes
|
||||
support for the cassette.play_count counter API, since individual requests
|
||||
aren't unique anymore. A cassette might contain the same request several
|
||||
times. Also removes secure overwrite feature since that was breaking
|
||||
overwriting files in Windows, and fixes a bug preventing request's
|
||||
automatic body decompression from working.
|
||||
* 0.3.5: Fix compatibility with requests 2.x
|
||||
* 0.3.4: Bugfix: close file before renaming it. This fixes an issue on
|
||||
Windows. Thanks @smallcode for the fix.
|
||||
* 0.3.3: Bugfix for error message when an unreigstered custom matcher was
|
||||
used
|
||||
* 0.3.2: Fix issue with new config syntax and the `match_on` parameter.
|
||||
Thanks, @chromy!
|
||||
* 0.3.1: Fix issue causing full paths to be sent on the HTTP request line.
|
||||
* 0.3.0: *Backwards incompatible release* - Added support for record modes,
|
||||
and changed the default recording behavior to the "once" record mode.
|
||||
Please see the documentation on record modes for more. Added support for
|
||||
custom request matching, and changed the default request matching behavior to
|
||||
match only on the URL and method. Also, improved the httplib mocking to add
|
||||
support for the `HTTPConnection.send()` method. This means that requests won't
|
||||
actually be sent until the response is read, since I need to record the entire
|
||||
request in order to match up the appropriate response. I don't think this
|
||||
should cause any issues unless you are sending requests without ever loading
|
||||
the response (which none of the standard httplib wrappers do, as far as I know.
|
||||
Thanks to @fatuhoku for some of the ideas and the motivation behind this
|
||||
release.
|
||||
* 0.2.1: Fixed missing modules in setup.py
|
||||
* 0.2.0: Added configuration API, which lets you configure some settings on
|
||||
VCR (see the README). Also, VCR no longer saves cassettes if they haven't
|
||||
changed at all and supports JSON as well as YAML (thanks @sirpengi). Added
|
||||
amazing new skeumorphic logo, thanks @hairarrow.
|
||||
* 0.1.0: *backwards incompatible release - delete your old cassette files*:
|
||||
This release adds the ability to access the cassette to make assertions on
|
||||
it, as well as a major code refactor thanks to @dlecocq. It also fixes a
|
||||
couple longstanding bugs with redirects and HTTPS. [#3 and #4]
|
||||
* 0.0.4: If you have libyaml installed, vcrpy will use the c bindings
|
||||
instead. Speed up your tests! Thanks @dlecocq
|
||||
* 0.0.3: Add support for requests 1.2.3. Support for older versions of
|
||||
requests dropped (thanks @vitormazzi and @bryanhelmig)
|
||||
* 0.0.2: Add support for requests / urllib3
|
||||
* 0.0.1: Initial Release
|
||||
|
||||
# License
|
||||
|
||||
#License
|
||||
This library uses the MIT license. See [LICENSE.txt](LICENSE.txt) for more details
|
||||
|
||||
60
setup.py
60
setup.py
@@ -13,34 +13,46 @@ class PyTest(TestCommand):
|
||||
self.test_suite = True
|
||||
|
||||
def run_tests(self):
|
||||
#import here, cause outside the eggs aren't loaded
|
||||
# import here, cause outside the eggs aren't loaded
|
||||
import pytest
|
||||
errno = pytest.main(self.test_args)
|
||||
sys.exit(errno)
|
||||
|
||||
setup(name='vcrpy',
|
||||
version='0.2.0',
|
||||
description="A Python port of Ruby's VCR to make mocking HTTP easier",
|
||||
author='Kevin McCarthy',
|
||||
author_email='me@kevinmccarthy.org',
|
||||
url='https://github.com/kevin1024/vcrpy',
|
||||
packages=[
|
||||
setup(
|
||||
name='vcrpy',
|
||||
version='1.0.2',
|
||||
description=(
|
||||
"Automatically mock your HTTP interactions to simplify and "
|
||||
"speed up testing"
|
||||
),
|
||||
author='Kevin McCarthy',
|
||||
author_email='me@kevinmccarthy.org',
|
||||
url='https://github.com/kevin1024/vcrpy',
|
||||
packages=[
|
||||
'vcr',
|
||||
'vcr.stubs'],
|
||||
package_dir={
|
||||
'vcr.stubs',
|
||||
'vcr.compat',
|
||||
'vcr.persisters',
|
||||
'vcr.serializers',
|
||||
],
|
||||
package_dir={
|
||||
'vcr': 'vcr',
|
||||
'vcr.stubs': 'vcr/stubs'},
|
||||
install_requires=['PyYAML'],
|
||||
license='MIT',
|
||||
tests_require=['pytest'],
|
||||
cmdclass={'test': PyTest},
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'Programming Language :: Python',
|
||||
'Topic :: Software Development :: Testing',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
],
|
||||
'vcr.stubs': 'vcr/stubs',
|
||||
'vcr.compat': 'vcr/compat',
|
||||
'vcr.persisters': 'vcr/persisters',
|
||||
},
|
||||
install_requires=['PyYAML', 'contextdecorator', 'six'],
|
||||
license='MIT',
|
||||
tests_require=['pytest', 'mock', 'pytest-localserver'],
|
||||
cmdclass={'test': PyTest},
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Software Development :: Testing',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import json
|
||||
|
||||
|
||||
def assert_cassette_empty(cass):
|
||||
assert len(cass) == 0
|
||||
assert cass.play_count == 0
|
||||
@@ -6,3 +9,11 @@ def assert_cassette_empty(cass):
|
||||
def assert_cassette_has_one_response(cass):
|
||||
assert len(cass) == 1
|
||||
assert cass.play_count == 1
|
||||
|
||||
|
||||
def assert_is_json(a_string):
|
||||
try:
|
||||
json.loads(a_string.decode('utf-8'))
|
||||
except Exception:
|
||||
assert False
|
||||
assert True
|
||||
|
||||
35
tests/fixtures/migration/new_cassette.json
vendored
Normal file
35
tests/fixtures/migration/new_cassette.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"version": 1,
|
||||
"interactions":
|
||||
[
|
||||
{
|
||||
"request": {
|
||||
"body": null,
|
||||
"headers": {
|
||||
"accept": ["*/*"],
|
||||
"accept-encoding": ["gzip, deflate, compress"],
|
||||
"user-agent": ["python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0"]
|
||||
},
|
||||
"method": "GET",
|
||||
"uri": "http://httpbin.org/ip"
|
||||
},
|
||||
"response": {
|
||||
"status": {
|
||||
"message": "OK",
|
||||
"code": 200
|
||||
},
|
||||
"headers": {
|
||||
"access-control-allow-origin": ["*"],
|
||||
"content-type": ["application/json"],
|
||||
"date": ["Mon, 21 Apr 2014 23:13:40 GMT"],
|
||||
"server": ["gunicorn/0.17.4"],
|
||||
"content-length": ["32"],
|
||||
"connection": ["keep-alive"]
|
||||
},
|
||||
"body": {
|
||||
"string": "{\n \"origin\": \"217.122.164.194\"\n}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
20
tests/fixtures/migration/new_cassette.yaml
vendored
Normal file
20
tests/fixtures/migration/new_cassette.yaml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
version: 1
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
accept: ['*/*']
|
||||
accept-encoding: ['gzip, deflate, compress']
|
||||
user-agent: ['python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0']
|
||||
method: GET
|
||||
uri: http://httpbin.org/ip
|
||||
response:
|
||||
body: {string: !!python/unicode "{\n \"origin\": \"217.122.164.194\"\n}"}
|
||||
headers:
|
||||
access-control-allow-origin: ['*']
|
||||
content-type: [application/json]
|
||||
date: ['Mon, 21 Apr 2014 23:06:09 GMT']
|
||||
server: [gunicorn/0.17.4]
|
||||
content-length: ['32']
|
||||
connection: [keep-alive]
|
||||
status: {code: 200, message: OK}
|
||||
1
tests/fixtures/migration/not_cassette.txt
vendored
Normal file
1
tests/fixtures/migration/not_cassette.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
This is not a cassette
|
||||
34
tests/fixtures/migration/old_cassette.json
vendored
Normal file
34
tests/fixtures/migration/old_cassette.json
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
[
|
||||
{
|
||||
"request": {
|
||||
"body": null,
|
||||
"protocol": "http",
|
||||
"method": "GET",
|
||||
"headers": {
|
||||
"accept-encoding": "gzip, deflate, compress",
|
||||
"accept": "*/*",
|
||||
"user-agent": "python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0"
|
||||
},
|
||||
"host": "httpbin.org",
|
||||
"path": "/ip",
|
||||
"port": 80
|
||||
},
|
||||
"response": {
|
||||
"status": {
|
||||
"message": "OK",
|
||||
"code": 200
|
||||
},
|
||||
"headers": [
|
||||
"access-control-allow-origin: *\r\n",
|
||||
"content-type: application/json\r\n",
|
||||
"date: Mon, 21 Apr 2014 23:13:40 GMT\r\n",
|
||||
"server: gunicorn/0.17.4\r\n",
|
||||
"content-length: 32\r\n",
|
||||
"connection: keep-alive\r\n"
|
||||
],
|
||||
"body": {
|
||||
"string": "{\n \"origin\": \"217.122.164.194\"\n}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
18
tests/fixtures/migration/old_cassette.yaml
vendored
Normal file
18
tests/fixtures/migration/old_cassette.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
- request: !!python/object:vcr.request.Request
|
||||
body: null
|
||||
headers: !!python/object/apply:__builtin__.frozenset
|
||||
- - !!python/tuple [accept-encoding, 'gzip, deflate, compress']
|
||||
- !!python/tuple [user-agent, python-requests/2.2.1 CPython/2.6.1 Darwin/10.8.0]
|
||||
- !!python/tuple [accept, '*/*']
|
||||
host: httpbin.org
|
||||
method: GET
|
||||
path: /ip
|
||||
port: 80
|
||||
protocol: http
|
||||
response:
|
||||
body: {string: !!python/unicode "{\n \"origin\": \"217.122.164.194\"\n}"}
|
||||
headers: [!!python/unicode "access-control-allow-origin: *\r\n", !!python/unicode "content-type:
|
||||
application/json\r\n", !!python/unicode "date: Mon, 21 Apr 2014 23:06:09 GMT\r\n",
|
||||
!!python/unicode "server: gunicorn/0.17.4\r\n", !!python/unicode "content-length:
|
||||
32\r\n", !!python/unicode "connection: keep-alive\r\n"]
|
||||
status: {code: 200, message: OK}
|
||||
265
tests/fixtures/wild/domain_redirect.yaml
vendored
265
tests/fixtures/wild/domain_redirect.yaml
vendored
@@ -1,131 +1,146 @@
|
||||
- request: !!python/object:vcr.request.Request
|
||||
version: 1
|
||||
interactions:
|
||||
- request:
|
||||
body: null
|
||||
headers: !!python/object/apply:__builtin__.frozenset
|
||||
- - !!python/tuple [Accept-Encoding, 'gzip, deflate, compress']
|
||||
- !!python/tuple [User-Agent, vcrpy-test]
|
||||
- !!python/tuple [Accept, '*/*']
|
||||
host: moz.com
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate, compress']
|
||||
User-Agent: ['vcrpy-test']
|
||||
method: GET
|
||||
path: /
|
||||
port: 80
|
||||
protocol: http
|
||||
uri: http://seomoz.org/
|
||||
response:
|
||||
body: {string: ''}
|
||||
headers:
|
||||
Location: ['http://moz.com/']
|
||||
Server: ['BigIP']
|
||||
Connection: ['Keep-Alive']
|
||||
Content-Length: ['0']
|
||||
status: {code: 301, message: Moved Permanently}
|
||||
- request:
|
||||
body: null
|
||||
headers:
|
||||
Accept: ['*/*']
|
||||
Accept-Encoding: ['gzip, deflate, compress']
|
||||
User-Agent: ['vcrpy-test']
|
||||
method: GET
|
||||
uri: http://moz.com/
|
||||
response:
|
||||
body:
|
||||
string: !!binary |
|
||||
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
|
||||
headers: {accept-ranges: bytes, age: '3593', cache-control: 'no-cache, must-revalidate,
|
||||
s-maxage=3600', connection: keep-alive, content-encoding: gzip, content-length: '5300',
|
||||
content-type: text/html, date: 'Sat, 10 Aug 2013 23:33:35 GMT', expires: 'Fri,
|
||||
15 Oct 2004 12:00:00 GMT', server: nginx, server-name: dalmozwww02.dal.moz.com,
|
||||
vary: Accept-Encoding, via: 1.1 varnish, x-varnish: 2027569899 2027508286}
|
||||
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
|
||||
headers:
|
||||
Server: [nginx]
|
||||
Content-Type: [text/html]
|
||||
Vary: [Accept-Encoding]
|
||||
Cache-Control: [no-cache]
|
||||
must-revalidate: ['s-maxage=3600']
|
||||
Expires: ['Fri 15 Oct 2004 12:00:00 GMT']
|
||||
Server-Name: ['dalmozwww01.dal.moz.com']
|
||||
Content-Encoding: [gzip]
|
||||
Content-Length: ['5683']
|
||||
Accept-Ranges: [bytes]
|
||||
Date: ['Sat, 11 Jan 2014 18:45:11 GMT']
|
||||
X-Varnish: [918768771 918700396]
|
||||
Age: ['3479']
|
||||
Via: ['1.1 varnish']
|
||||
Connection: [keep-alive]
|
||||
status: {code: 200, message: OK}
|
||||
- request: !!python/object:vcr.request.Request
|
||||
body: null
|
||||
headers: !!python/object/apply:__builtin__.frozenset
|
||||
- - !!python/tuple [Accept-Encoding, 'gzip, deflate, compress']
|
||||
- !!python/tuple [User-Agent, vcrpy-test]
|
||||
- !!python/tuple [Accept, '*/*']
|
||||
host: seomoz.org
|
||||
method: GET
|
||||
path: /
|
||||
port: 80
|
||||
protocol: http
|
||||
response:
|
||||
body: {string: "<html>\r\n<head><title>301 Moved Permanently</title></head>\r\n<body
|
||||
bgcolor=\"white\">\r\n<center><h1>301 Moved Permanently</h1></center>\r\n<hr><center>nginx</center>\r\n</body>\r\n</html>\r\n"}
|
||||
headers: {accept-ranges: bytes, age: '0', connection: keep-alive, content-length: '178',
|
||||
content-type: text/html, date: 'Sat, 10 Aug 2013 23:33:35 GMT', location: 'http://moz.com/',
|
||||
server: nginx, server-name: dalmozwww01.dal.moz.com, via: 1.1 varnish, x-varnish: '2027569892'}
|
||||
status: {code: 301, message: Moved Permanently}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# External imports
|
||||
import os
|
||||
import urllib2
|
||||
from six.moves.urllib.request import urlopen
|
||||
|
||||
# Internal imports
|
||||
import vcr
|
||||
@@ -16,7 +16,7 @@ def test_nonexistent_directory(tmpdir):
|
||||
|
||||
# Run VCR to create dir and cassette file
|
||||
with vcr.use_cassette(str(tmpdir.join('nonexistent', 'cassette.yml'))):
|
||||
urllib2.urlopen('http://httpbin.org/').read()
|
||||
urlopen('http://httpbin.org/').read()
|
||||
|
||||
# This should have made the file and the directory
|
||||
assert os.path.exists(str(tmpdir.join('nonexistent', 'cassette.yml')))
|
||||
@@ -25,11 +25,11 @@ def test_nonexistent_directory(tmpdir):
|
||||
def test_unpatch(tmpdir):
|
||||
'''Ensure that our cassette gets unpatched when we're done'''
|
||||
with vcr.use_cassette(str(tmpdir.join('unpatch.yaml'))) as cass:
|
||||
urllib2.urlopen('http://httpbin.org/').read()
|
||||
urlopen('http://httpbin.org/').read()
|
||||
|
||||
# Make the same request, and assert that we haven't served any more
|
||||
# requests out of cache
|
||||
urllib2.urlopen('http://httpbin.org/').read()
|
||||
urlopen('http://httpbin.org/').read()
|
||||
assert cass.play_count == 0
|
||||
|
||||
|
||||
@@ -38,10 +38,10 @@ def test_basic_use(tmpdir):
|
||||
Copied from the docs
|
||||
'''
|
||||
with vcr.use_cassette('fixtures/vcr_cassettes/synopsis.yaml'):
|
||||
response = urllib2.urlopen(
|
||||
response = urlopen(
|
||||
'http://www.iana.org/domains/reserved'
|
||||
).read()
|
||||
assert 'Example domains' in response
|
||||
assert b'Example domains' in response
|
||||
|
||||
|
||||
def test_basic_json_use(tmpdir):
|
||||
@@ -50,8 +50,8 @@ def test_basic_json_use(tmpdir):
|
||||
'''
|
||||
test_fixture = 'fixtures/vcr_cassettes/synopsis.json'
|
||||
with vcr.use_cassette(test_fixture, serializer='json'):
|
||||
response = urllib2.urlopen('http://httpbin.org/').read()
|
||||
assert 'difficult sometimes' in response
|
||||
response = urlopen('http://httpbin.org/').read()
|
||||
assert b'difficult sometimes' in response
|
||||
|
||||
|
||||
def test_patched_content(tmpdir):
|
||||
@@ -60,16 +60,16 @@ def test_patched_content(tmpdir):
|
||||
request
|
||||
'''
|
||||
with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass:
|
||||
response = urllib2.urlopen('http://httpbin.org/').read()
|
||||
response = urlopen('http://httpbin.org/').read()
|
||||
assert cass.play_count == 0
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass:
|
||||
response2 = urllib2.urlopen('http://httpbin.org/').read()
|
||||
response2 = urlopen('http://httpbin.org/').read()
|
||||
assert cass.play_count == 1
|
||||
cass._save(force=True)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('synopsis.yaml'))) as cass:
|
||||
response3 = urllib2.urlopen('http://httpbin.org/').read()
|
||||
response3 = urlopen('http://httpbin.org/').read()
|
||||
assert cass.play_count == 1
|
||||
|
||||
assert response == response2
|
||||
@@ -85,16 +85,16 @@ def test_patched_content_json(tmpdir):
|
||||
testfile = str(tmpdir.join('synopsis.json'))
|
||||
|
||||
with vcr.use_cassette(testfile) as cass:
|
||||
response = urllib2.urlopen('http://httpbin.org/').read()
|
||||
response = urlopen('http://httpbin.org/').read()
|
||||
assert cass.play_count == 0
|
||||
|
||||
with vcr.use_cassette(testfile) as cass:
|
||||
response2 = urllib2.urlopen('http://httpbin.org/').read()
|
||||
response2 = urlopen('http://httpbin.org/').read()
|
||||
assert cass.play_count == 1
|
||||
cass._save(force=True)
|
||||
|
||||
with vcr.use_cassette(testfile) as cass:
|
||||
response3 = urllib2.urlopen('http://httpbin.org/').read()
|
||||
response3 = urlopen('http://httpbin.org/').read()
|
||||
assert cass.play_count == 1
|
||||
|
||||
assert response == response2
|
||||
|
||||
72
tests/integration/test_boto.py
Normal file
72
tests/integration/test_boto.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import pytest
|
||||
boto = pytest.importorskip("boto")
|
||||
import boto
|
||||
import boto.iam
|
||||
from boto.s3.connection import S3Connection
|
||||
from boto.s3.key import Key
|
||||
from ConfigParser import DuplicateSectionError
|
||||
import vcr
|
||||
|
||||
def test_boto_stubs(tmpdir):
|
||||
with vcr.use_cassette(str(tmpdir.join('boto-stubs.yml'))):
|
||||
# Perform the imports within the patched context so that
|
||||
# CertValidatingHTTPSConnection refers to the patched version.
|
||||
from boto.https_connection import CertValidatingHTTPSConnection
|
||||
from vcr.stubs.boto_stubs import VCRCertValidatingHTTPSConnection
|
||||
# Prove that the class was patched by the stub and that we can instantiate it.
|
||||
assert CertValidatingHTTPSConnection is VCRCertValidatingHTTPSConnection
|
||||
CertValidatingHTTPSConnection('hostname.does.not.matter')
|
||||
|
||||
def test_boto_without_vcr():
|
||||
s3_conn = S3Connection()
|
||||
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
|
||||
k = Key(s3_bucket)
|
||||
k.key = 'test.txt'
|
||||
k.set_contents_from_string('hello world i am a string')
|
||||
|
||||
def test_boto_medium_difficulty(tmpdir):
|
||||
s3_conn = S3Connection()
|
||||
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
|
||||
with vcr.use_cassette(str(tmpdir.join('boto-medium.yml'))) as cass:
|
||||
k = Key(s3_bucket)
|
||||
k.key = 'test.txt'
|
||||
k.set_contents_from_string('hello world i am a string')
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('boto-medium.yml'))) as cass:
|
||||
k = Key(s3_bucket)
|
||||
k.key = 'test.txt'
|
||||
k.set_contents_from_string('hello world i am a string')
|
||||
|
||||
|
||||
def test_boto_hardcore_mode(tmpdir):
|
||||
with vcr.use_cassette(str(tmpdir.join('boto-hardcore.yml'))) as cass:
|
||||
s3_conn = S3Connection()
|
||||
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
|
||||
k = Key(s3_bucket)
|
||||
k.key = 'test.txt'
|
||||
k.set_contents_from_string('hello world i am a string')
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('boto-hardcore.yml'))) as cass:
|
||||
s3_conn = S3Connection()
|
||||
s3_bucket = s3_conn.get_bucket('boto-demo-1394171994') # a bucket you can access
|
||||
k = Key(s3_bucket)
|
||||
k.key = 'test.txt'
|
||||
k.set_contents_from_string('hello world i am a string')
|
||||
|
||||
def test_boto_iam(tmpdir):
|
||||
try:
|
||||
boto.config.add_section('Boto')
|
||||
except DuplicateSectionError:
|
||||
pass
|
||||
# Ensure that boto uses HTTPS
|
||||
boto.config.set('Boto', 'is_secure', 'true')
|
||||
# Ensure that boto uses CertValidatingHTTPSConnection
|
||||
boto.config.set('Boto', 'https_validate_certificates', 'true')
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('boto-iam.yml'))) as cass:
|
||||
iam_conn = boto.iam.connect_to_region('universal')
|
||||
iam_conn.get_all_users()
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('boto-iam.yml'))) as cass:
|
||||
iam_conn = boto.iam.connect_to_region('universal')
|
||||
iam_conn.get_all_users()
|
||||
@@ -1,7 +1,8 @@
|
||||
import os
|
||||
import json
|
||||
import urllib2
|
||||
import pytest
|
||||
import vcr
|
||||
from six.moves.urllib.request import urlopen
|
||||
|
||||
|
||||
def test_set_serializer_default_config(tmpdir):
|
||||
@@ -9,7 +10,7 @@ def test_set_serializer_default_config(tmpdir):
|
||||
|
||||
with my_vcr.use_cassette(str(tmpdir.join('test.json'))):
|
||||
assert my_vcr.serializer == 'json'
|
||||
urllib2.urlopen('http://httpbin.org/get')
|
||||
urlopen('http://httpbin.org/get')
|
||||
|
||||
with open(str(tmpdir.join('test.json'))) as f:
|
||||
assert json.loads(f.read())
|
||||
@@ -19,7 +20,7 @@ def test_default_set_cassette_library_dir(tmpdir):
|
||||
my_vcr = vcr.VCR(cassette_library_dir=str(tmpdir.join('subdir')))
|
||||
|
||||
with my_vcr.use_cassette('test.json'):
|
||||
urllib2.urlopen('http://httpbin.org/get')
|
||||
urlopen('http://httpbin.org/get')
|
||||
|
||||
assert os.path.exists(str(tmpdir.join('subdir').join('test.json')))
|
||||
|
||||
@@ -30,7 +31,28 @@ def test_override_set_cassette_library_dir(tmpdir):
|
||||
cld = str(tmpdir.join('subdir2'))
|
||||
|
||||
with my_vcr.use_cassette('test.json', cassette_library_dir=cld):
|
||||
urllib2.urlopen('http://httpbin.org/get')
|
||||
urlopen('http://httpbin.org/get')
|
||||
|
||||
assert os.path.exists(str(tmpdir.join('subdir2').join('test.json')))
|
||||
assert not os.path.exists(str(tmpdir.join('subdir').join('test.json')))
|
||||
|
||||
|
||||
def test_override_match_on(tmpdir):
|
||||
my_vcr = vcr.VCR(match_on=['method'])
|
||||
|
||||
with my_vcr.use_cassette(str(tmpdir.join('test.json'))):
|
||||
urlopen('http://httpbin.org/')
|
||||
|
||||
with my_vcr.use_cassette(str(tmpdir.join('test.json'))) as cass:
|
||||
urlopen('http://httpbin.org/get')
|
||||
|
||||
assert len(cass) == 1
|
||||
assert cass.play_count == 1
|
||||
|
||||
|
||||
def test_missing_matcher():
|
||||
my_vcr = vcr.VCR()
|
||||
my_vcr.register_matcher("awesome", object)
|
||||
with pytest.raises(KeyError):
|
||||
with my_vcr.use_cassette("test.yaml", match_on=['notawesome']):
|
||||
pass
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
# External imports
|
||||
import os
|
||||
import urllib2
|
||||
import time
|
||||
from six.moves.urllib.request import urlopen
|
||||
|
||||
# Internal imports
|
||||
import vcr
|
||||
@@ -17,12 +17,12 @@ def test_disk_saver_nowrite(tmpdir):
|
||||
'''
|
||||
fname = str(tmpdir.join('synopsis.yaml'))
|
||||
with vcr.use_cassette(fname) as cass:
|
||||
urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert cass.play_count == 0
|
||||
last_mod = os.path.getmtime(fname)
|
||||
|
||||
with vcr.use_cassette(fname) as cass:
|
||||
urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert cass.play_count == 1
|
||||
assert cass.dirty is False
|
||||
last_mod2 = os.path.getmtime(fname)
|
||||
@@ -37,7 +37,7 @@ def test_disk_saver_write(tmpdir):
|
||||
'''
|
||||
fname = str(tmpdir.join('synopsis.yaml'))
|
||||
with vcr.use_cassette(fname) as cass:
|
||||
urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
urlopen('http://www.iana.org/domains/reserved').read()
|
||||
assert cass.play_count == 0
|
||||
last_mod = os.path.getmtime(fname)
|
||||
|
||||
@@ -45,9 +45,9 @@ def test_disk_saver_write(tmpdir):
|
||||
# the mtime doesn't change
|
||||
time.sleep(1)
|
||||
|
||||
with vcr.use_cassette(fname) as cass:
|
||||
urllib2.urlopen('http://www.iana.org/domains/reserved').read()
|
||||
urllib2.urlopen('http://httpbin.org/').read()
|
||||
with vcr.use_cassette(fname, record_mode='any') as cass:
|
||||
urlopen('http://www.iana.org/domains/reserved').read()
|
||||
urlopen('http://httpbin.org/').read()
|
||||
assert cass.play_count == 1
|
||||
assert cass.dirty
|
||||
last_mod2 = os.path.getmtime(fname)
|
||||
|
||||
68
tests/integration/test_filter.py
Normal file
68
tests/integration/test_filter.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import base64
|
||||
import pytest
|
||||
from six.moves.urllib.request import urlopen, Request
|
||||
from six.moves.urllib.error import HTTPError
|
||||
import vcr
|
||||
|
||||
|
||||
def _request_with_auth(url, username, password):
|
||||
request = Request(url)
|
||||
base64string = base64.b64encode(
|
||||
username.encode('ascii') + b':' + password.encode('ascii')
|
||||
)
|
||||
request.add_header(b"Authorization", b"Basic " + base64string)
|
||||
return urlopen(request)
|
||||
|
||||
|
||||
def _find_header(cassette, header):
|
||||
for request in cassette.requests:
|
||||
for k in request.headers:
|
||||
if header.lower() == k.lower():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def test_filter_basic_auth(tmpdir):
|
||||
url = 'http://httpbin.org/basic-auth/user/passwd'
|
||||
cass_file = str(tmpdir.join('basic_auth_filter.yaml'))
|
||||
my_vcr = vcr.VCR(match_on=['uri', 'method', 'headers'])
|
||||
# 2 requests, one with auth failure and one with auth success
|
||||
with my_vcr.use_cassette(cass_file, filter_headers=['authorization']):
|
||||
with pytest.raises(HTTPError):
|
||||
resp = _request_with_auth(url, 'user', 'wrongpasswd')
|
||||
assert resp.getcode() == 401
|
||||
resp = _request_with_auth(url, 'user', 'passwd')
|
||||
assert resp.getcode() == 200
|
||||
# make same 2 requests, this time both served from cassette.
|
||||
with my_vcr.use_cassette(cass_file, filter_headers=['authorization']) as cass:
|
||||
with pytest.raises(HTTPError):
|
||||
resp = _request_with_auth(url, 'user', 'wrongpasswd')
|
||||
assert resp.getcode() == 401
|
||||
resp = _request_with_auth(url, 'user', 'passwd')
|
||||
assert resp.getcode() == 200
|
||||
# authorization header should not have been recorded
|
||||
assert not _find_header(cass, 'authorization')
|
||||
assert len(cass) == 2
|
||||
|
||||
|
||||
def test_filter_querystring(tmpdir):
|
||||
url = 'http://httpbin.org/?foo=bar'
|
||||
cass_file = str(tmpdir.join('filter_qs.yaml'))
|
||||
with vcr.use_cassette(cass_file, filter_query_parameters=['foo']):
|
||||
urlopen(url)
|
||||
with vcr.use_cassette(cass_file, filter_query_parameters=['foo']) as cass:
|
||||
urlopen(url)
|
||||
assert 'foo' not in cass.requests[0].url
|
||||
|
||||
def test_filter_callback(tmpdir):
|
||||
url = 'http://httpbin.org/get'
|
||||
cass_file = str(tmpdir.join('basic_auth_filter.yaml'))
|
||||
def before_record_cb(request):
|
||||
if request.path != '/get':
|
||||
return request
|
||||
my_vcr = vcr.VCR(
|
||||
before_record = before_record_cb,
|
||||
)
|
||||
with my_vcr.use_cassette(cass_file, filter_headers=['authorization']) as cass:
|
||||
urlopen(url)
|
||||
assert len(cass) == 0
|
||||
140
tests/integration/test_httplib2.py
Normal file
140
tests/integration/test_httplib2.py
Normal file
@@ -0,0 +1,140 @@
|
||||
'''Integration tests with httplib2'''
|
||||
# coding=utf-8
|
||||
|
||||
# External imports
|
||||
from six.moves.urllib_parse import urlencode
|
||||
import pytest
|
||||
|
||||
# Internal imports
|
||||
import vcr
|
||||
|
||||
from assertions import assert_cassette_has_one_response
|
||||
|
||||
httplib2 = pytest.importorskip("httplib2")
|
||||
|
||||
|
||||
@pytest.fixture(params=["https", "http"])
|
||||
def scheme(request):
|
||||
"""
|
||||
Fixture that returns both http and https
|
||||
"""
|
||||
return request.param
|
||||
|
||||
|
||||
def test_response_code(scheme, tmpdir):
|
||||
'''Ensure we can read a response code from a fetch'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
|
||||
resp, _ = httplib2.Http().request(url)
|
||||
code = resp.status
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
|
||||
resp, _ = httplib2.Http().request(url)
|
||||
assert code == resp.status
|
||||
|
||||
|
||||
def test_random_body(scheme, tmpdir):
|
||||
'''Ensure we can read the content, and that it's served from cache'''
|
||||
url = scheme + '://httpbin.org/bytes/1024'
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
|
||||
_, content = httplib2.Http().request(url)
|
||||
body = content
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
|
||||
_, content = httplib2.Http().request(url)
|
||||
assert body == content
|
||||
|
||||
|
||||
def test_response_headers(scheme, tmpdir):
|
||||
'''Ensure we can get information from the response'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
resp, _ = httplib2.Http().request(url)
|
||||
headers = resp.items()
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
resp, _ = httplib2.Http().request(url)
|
||||
assert headers == resp.items()
|
||||
|
||||
|
||||
def test_multiple_requests(scheme, tmpdir):
|
||||
'''Ensure that we can cache multiple requests'''
|
||||
urls = [
|
||||
scheme + '://httpbin.org/',
|
||||
scheme + '://httpbin.org/',
|
||||
scheme + '://httpbin.org/get',
|
||||
scheme + '://httpbin.org/bytes/1024'
|
||||
]
|
||||
with vcr.use_cassette(str(tmpdir.join('multiple.yaml'))) as cass:
|
||||
[httplib2.Http().request(url) for url in urls]
|
||||
assert len(cass) == len(urls)
|
||||
|
||||
|
||||
def test_get_data(scheme, tmpdir):
|
||||
'''Ensure that it works with query data'''
|
||||
data = urlencode({'some': 1, 'data': 'here'})
|
||||
url = scheme + '://httpbin.org/get?' + data
|
||||
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
|
||||
_, res1 = httplib2.Http().request(url)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
|
||||
_, res2 = httplib2.Http().request(url)
|
||||
|
||||
assert res1 == res2
|
||||
|
||||
|
||||
def test_post_data(scheme, tmpdir):
|
||||
'''Ensure that it works when posting data'''
|
||||
data = urlencode({'some': 1, 'data': 'here'})
|
||||
url = scheme + '://httpbin.org/post'
|
||||
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
|
||||
_, res1 = httplib2.Http().request(url, "POST", data)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
|
||||
_, res2 = httplib2.Http().request(url, "POST", data)
|
||||
|
||||
assert res1 == res2
|
||||
assert_cassette_has_one_response(cass)
|
||||
|
||||
|
||||
def test_post_unicode_data(scheme, tmpdir):
|
||||
'''Ensure that it works when posting unicode data'''
|
||||
data = urlencode({'snowman': u'☃'.encode('utf-8')})
|
||||
url = scheme + '://httpbin.org/post'
|
||||
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
|
||||
_, res1 = httplib2.Http().request(url, "POST", data)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
|
||||
_, res2 = httplib2.Http().request(url, "POST", data)
|
||||
|
||||
assert res1 == res2
|
||||
assert_cassette_has_one_response(cass)
|
||||
|
||||
|
||||
def test_cross_scheme(tmpdir):
|
||||
'''Ensure that requests between schemes are treated separately'''
|
||||
# First fetch a url under https, and then again under https and then
|
||||
# ensure that we haven't served anything out of cache, and we have two
|
||||
# requests / response pairs in the cassette
|
||||
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass:
|
||||
httplib2.Http().request('https://httpbin.org/')
|
||||
httplib2.Http().request('http://httpbin.org/')
|
||||
assert len(cass) == 2
|
||||
assert cass.play_count == 0
|
||||
|
||||
|
||||
def test_decorator(scheme, tmpdir):
|
||||
'''Test the decorator version of VCR.py'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
|
||||
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
|
||||
def inner1():
|
||||
resp, _ = httplib2.Http().request(url)
|
||||
return resp['status']
|
||||
|
||||
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
|
||||
def inner2():
|
||||
resp, _ = httplib2.Http().request(url)
|
||||
return resp['status']
|
||||
|
||||
assert inner1() == inner2()
|
||||
55
tests/integration/test_ignore.py
Normal file
55
tests/integration/test_ignore.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import base64
|
||||
import pytest
|
||||
from six.moves.urllib.request import urlopen, Request
|
||||
from six.moves.urllib.error import HTTPError
|
||||
import vcr
|
||||
|
||||
|
||||
def test_ignore_localhost(tmpdir, httpserver):
|
||||
httpserver.serve_content('Hello!')
|
||||
cass_file = str(tmpdir.join('filter_qs.yaml'))
|
||||
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
||||
urlopen(httpserver.url)
|
||||
assert len(cass) == 0
|
||||
urlopen('http://httpbin.org')
|
||||
assert len(cass) == 1
|
||||
|
||||
|
||||
def test_ignore_httpbin(tmpdir, httpserver):
|
||||
httpserver.serve_content('Hello!')
|
||||
cass_file = str(tmpdir.join('filter_qs.yaml'))
|
||||
with vcr.use_cassette(
|
||||
cass_file,
|
||||
ignore_hosts=['httpbin.org']
|
||||
) as cass:
|
||||
urlopen('http://httpbin.org')
|
||||
assert len(cass) == 0
|
||||
urlopen(httpserver.url)
|
||||
assert len(cass) == 1
|
||||
|
||||
|
||||
def test_ignore_localhost_and_httpbin(tmpdir, httpserver):
|
||||
httpserver.serve_content('Hello!')
|
||||
cass_file = str(tmpdir.join('filter_qs.yaml'))
|
||||
with vcr.use_cassette(
|
||||
cass_file,
|
||||
ignore_hosts=['httpbin.org'],
|
||||
ignore_localhost=True
|
||||
) as cass:
|
||||
urlopen('http://httpbin.org')
|
||||
urlopen(httpserver.url)
|
||||
assert len(cass) == 0
|
||||
|
||||
def test_ignore_localhost_twice(tmpdir, httpserver):
|
||||
httpserver.serve_content('Hello!')
|
||||
cass_file = str(tmpdir.join('filter_qs.yaml'))
|
||||
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
||||
urlopen(httpserver.url)
|
||||
assert len(cass) == 0
|
||||
urlopen('http://httpbin.org')
|
||||
assert len(cass) == 1
|
||||
with vcr.use_cassette(cass_file, ignore_localhost=True) as cass:
|
||||
assert len(cass) == 1
|
||||
urlopen(httpserver.url)
|
||||
urlopen('http://httpbin.org')
|
||||
assert len(cass) == 1
|
||||
99
tests/integration/test_matchers.py
Normal file
99
tests/integration/test_matchers.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import vcr
|
||||
import pytest
|
||||
from six.moves.urllib.request import urlopen
|
||||
|
||||
|
||||
DEFAULT_URI = 'http://httpbin.org/get?p1=q1&p2=q2' # base uri for testing
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cassette(tmpdir):
|
||||
"""
|
||||
Helper fixture used to prepare the cassete
|
||||
returns path to the recorded cassette
|
||||
"""
|
||||
cassette_path = str(tmpdir.join('test.yml'))
|
||||
with vcr.use_cassette(cassette_path, record_mode='all'):
|
||||
urlopen(DEFAULT_URI)
|
||||
return cassette_path
|
||||
|
||||
|
||||
@pytest.mark.parametrize("matcher, matching_uri, not_matching_uri", [
|
||||
('uri',
|
||||
'http://httpbin.org/get?p1=q1&p2=q2',
|
||||
'http://httpbin.org/get?p2=q2&p1=q1'),
|
||||
('scheme',
|
||||
'http://google.com/post?a=b',
|
||||
'https://httpbin.org/get?p1=q1&p2=q2'),
|
||||
('host',
|
||||
'https://httpbin.org/post?a=b',
|
||||
'http://google.com/get?p1=q1&p2=q2'),
|
||||
('port',
|
||||
'https://google.com:80/post?a=b',
|
||||
'http://httpbin.org:5000/get?p1=q1&p2=q2'),
|
||||
('path',
|
||||
'https://google.com/get?a=b',
|
||||
'http://httpbin.org/post?p1=q1&p2=q2'),
|
||||
('query',
|
||||
'https://google.com/get?p2=q2&p1=q1',
|
||||
'http://httpbin.org/get?p1=q1&a=b')
|
||||
])
|
||||
def test_matchers(cassette, matcher, matching_uri, not_matching_uri):
|
||||
# play cassette with default uri
|
||||
with vcr.use_cassette(cassette, match_on=[matcher]) as cass:
|
||||
urlopen(DEFAULT_URI)
|
||||
assert cass.play_count == 1
|
||||
|
||||
# play cassette with matching on uri
|
||||
with vcr.use_cassette(cassette, match_on=[matcher]) as cass:
|
||||
urlopen(matching_uri)
|
||||
assert cass.play_count == 1
|
||||
|
||||
# play cassette with not matching on uri, it should fail
|
||||
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
||||
with vcr.use_cassette(cassette, match_on=[matcher]) as cass:
|
||||
urlopen(not_matching_uri)
|
||||
|
||||
|
||||
def test_method_matcher(cassette):
|
||||
# play cassette with matching on method
|
||||
with vcr.use_cassette(cassette, match_on=['method']) as cass:
|
||||
urlopen('https://google.com/get?a=b')
|
||||
assert cass.play_count == 1
|
||||
|
||||
# should fail if method does not match
|
||||
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
||||
with vcr.use_cassette(cassette, match_on=['method']) as cass:
|
||||
# is a POST request
|
||||
urlopen(DEFAULT_URI, data=b'')
|
||||
|
||||
|
||||
@pytest.mark.parametrize("uri", [
|
||||
DEFAULT_URI,
|
||||
'http://httpbin.org/get?p2=q2&p1=q1',
|
||||
'http://httpbin.org/get?p2=q2&p1=q1',
|
||||
])
|
||||
def test_default_matcher_matches(cassette, uri):
|
||||
with vcr.use_cassette(cassette) as cass:
|
||||
urlopen(uri)
|
||||
assert cass.play_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("uri", [
|
||||
'https://httpbin.org/get?p1=q1&p2=q2',
|
||||
'http://google.com/get?p1=q1&p2=q2',
|
||||
'http://httpbin.org:5000/get?p1=q1&p2=q2',
|
||||
'http://httpbin.org/post?p1=q1&p2=q2',
|
||||
'http://httpbin.org/get?p1=q1&a=b'
|
||||
])
|
||||
def test_default_matcher_does_not_match(cassette, uri):
|
||||
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
||||
with vcr.use_cassette(cassette):
|
||||
urlopen(uri)
|
||||
|
||||
|
||||
def test_default_matcher_does_not_match_on_method(cassette):
|
||||
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
|
||||
with vcr.use_cassette(cassette):
|
||||
# is a POST request
|
||||
urlopen(DEFAULT_URI, data=b'')
|
||||
20
tests/integration/test_multiple.py
Normal file
20
tests/integration/test_multiple.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import pytest
|
||||
import vcr
|
||||
from six.moves.urllib.request import urlopen
|
||||
|
||||
|
||||
def test_making_extra_request_raises_exception(tmpdir):
|
||||
# make two requests in the first request that are considered
|
||||
# identical (since the match is based on method)
|
||||
with vcr.use_cassette(str(tmpdir.join('test.json')), match_on=['method']):
|
||||
urlopen('http://httpbin.org/status/200')
|
||||
urlopen('http://httpbin.org/status/201')
|
||||
|
||||
# Now, try to make three requests. The first two should return the
|
||||
# correct status codes in order, and the third should raise an
|
||||
# exception.
|
||||
with vcr.use_cassette(str(tmpdir.join('test.json')), match_on=['method']):
|
||||
assert urlopen('http://httpbin.org/status/200').getcode() == 200
|
||||
assert urlopen('http://httpbin.org/status/201').getcode() == 201
|
||||
with pytest.raises(Exception):
|
||||
urlopen('http://httpbin.org/status/200')
|
||||
118
tests/integration/test_record_mode.py
Normal file
118
tests/integration/test_record_mode.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import os
|
||||
import pytest
|
||||
import vcr
|
||||
from six.moves.urllib.request import urlopen
|
||||
|
||||
|
||||
def test_once_record_mode(tmpdir):
|
||||
testfile = str(tmpdir.join('recordmode.yml'))
|
||||
with vcr.use_cassette(testfile, record_mode="once"):
|
||||
# cassette file doesn't exist, so create.
|
||||
response = urlopen('http://httpbin.org/').read()
|
||||
|
||||
with vcr.use_cassette(testfile, record_mode="once") as cass:
|
||||
# make the same request again
|
||||
response = urlopen('http://httpbin.org/').read()
|
||||
|
||||
# the first time, it's played from the cassette.
|
||||
# but, try to access something else from the same cassette, and an
|
||||
# exception is raised.
|
||||
with pytest.raises(Exception):
|
||||
response = urlopen('http://httpbin.org/get').read()
|
||||
|
||||
|
||||
def test_once_record_mode_two_times(tmpdir):
|
||||
testfile = str(tmpdir.join('recordmode.yml'))
|
||||
with vcr.use_cassette(testfile, record_mode="once"):
|
||||
# get two of the same file
|
||||
response1 = urlopen('http://httpbin.org/').read()
|
||||
response2 = urlopen('http://httpbin.org/').read()
|
||||
|
||||
with vcr.use_cassette(testfile, record_mode="once") as cass:
|
||||
# do it again
|
||||
response = urlopen('http://httpbin.org/').read()
|
||||
response = urlopen('http://httpbin.org/').read()
|
||||
|
||||
|
||||
def test_once_mode_three_times(tmpdir):
|
||||
testfile = str(tmpdir.join('recordmode.yml'))
|
||||
with vcr.use_cassette(testfile, record_mode="once"):
|
||||
# get three of the same file
|
||||
response1 = urlopen('http://httpbin.org/').read()
|
||||
response2 = urlopen('http://httpbin.org/').read()
|
||||
response2 = urlopen('http://httpbin.org/').read()
|
||||
|
||||
|
||||
def test_new_episodes_record_mode(tmpdir):
|
||||
testfile = str(tmpdir.join('recordmode.yml'))
|
||||
|
||||
with vcr.use_cassette(testfile, record_mode="new_episodes"):
|
||||
# cassette file doesn't exist, so create.
|
||||
response = urlopen('http://httpbin.org/').read()
|
||||
|
||||
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass:
|
||||
# make the same request again
|
||||
response = urlopen('http://httpbin.org/').read()
|
||||
|
||||
# all responses have been played
|
||||
assert cass.all_played
|
||||
|
||||
# in the "new_episodes" record mode, we can add more requests to
|
||||
# a cassette without repurcussions.
|
||||
response = urlopen('http://httpbin.org/get').read()
|
||||
|
||||
# one of the responses has been played
|
||||
assert cass.play_count == 1
|
||||
|
||||
# not all responses have been played
|
||||
assert not cass.all_played
|
||||
|
||||
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass:
|
||||
# the cassette should now have 2 responses
|
||||
assert len(cass.responses) == 2
|
||||
|
||||
|
||||
def test_all_record_mode(tmpdir):
|
||||
testfile = str(tmpdir.join('recordmode.yml'))
|
||||
|
||||
with vcr.use_cassette(testfile, record_mode="all"):
|
||||
# cassette file doesn't exist, so create.
|
||||
response = urlopen('http://httpbin.org/').read()
|
||||
|
||||
with vcr.use_cassette(testfile, record_mode="all") as cass:
|
||||
# make the same request again
|
||||
response = urlopen('http://httpbin.org/').read()
|
||||
|
||||
# in the "all" record mode, we can add more requests to
|
||||
# a cassette without repurcussions.
|
||||
response = urlopen('http://httpbin.org/get').read()
|
||||
|
||||
# The cassette was never actually played, even though it existed.
|
||||
# that's because, in "all" mode, the requests all go directly to
|
||||
# the source and bypass the cassette.
|
||||
assert cass.play_count == 0
|
||||
|
||||
|
||||
def test_none_record_mode(tmpdir):
|
||||
# Cassette file doesn't exist, yet we are trying to make a request.
|
||||
# raise hell.
|
||||
testfile = str(tmpdir.join('recordmode.yml'))
|
||||
with vcr.use_cassette(testfile, record_mode="none"):
|
||||
with pytest.raises(Exception):
|
||||
response = urlopen('http://httpbin.org/').read()
|
||||
|
||||
|
||||
def test_none_record_mode_with_existing_cassette(tmpdir):
|
||||
# create a cassette file
|
||||
testfile = str(tmpdir.join('recordmode.yml'))
|
||||
|
||||
with vcr.use_cassette(testfile, record_mode="all"):
|
||||
response = urlopen('http://httpbin.org/').read()
|
||||
|
||||
# play from cassette file
|
||||
with vcr.use_cassette(testfile, record_mode="none") as cass:
|
||||
response = urlopen('http://httpbin.org/').read()
|
||||
assert cass.play_count == 1
|
||||
# but if I try to hit the net, raise an exception.
|
||||
with pytest.raises(Exception):
|
||||
response = urlopen('http://httpbin.org/get').read()
|
||||
36
tests/integration/test_register_matcher.py
Normal file
36
tests/integration/test_register_matcher.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import vcr
|
||||
from six.moves.urllib.request import urlopen
|
||||
|
||||
|
||||
def true_matcher(r1, r2):
|
||||
return True
|
||||
|
||||
|
||||
def false_matcher(r1, r2):
|
||||
return False
|
||||
|
||||
|
||||
def test_registered_true_matcher(tmpdir):
|
||||
my_vcr = vcr.VCR()
|
||||
my_vcr.register_matcher('true', true_matcher)
|
||||
testfile = str(tmpdir.join('test.yml'))
|
||||
with my_vcr.use_cassette(testfile, match_on=['true']) as cass:
|
||||
# These 2 different urls are stored as the same request
|
||||
urlopen('http://httpbin.org/')
|
||||
urlopen('https://httpbin.org/get')
|
||||
|
||||
with my_vcr.use_cassette(testfile, match_on=['true']) as cass:
|
||||
# I can get the response twice even though I only asked for it once
|
||||
urlopen('http://httpbin.org/get')
|
||||
urlopen('https://httpbin.org/get')
|
||||
|
||||
|
||||
def test_registered_false_matcher(tmpdir):
|
||||
my_vcr = vcr.VCR()
|
||||
my_vcr.register_matcher('false', false_matcher)
|
||||
testfile = str(tmpdir.join('test.yml'))
|
||||
with my_vcr.use_cassette(testfile, match_on=['false']) as cass:
|
||||
# These 2 different urls are stored as different requests
|
||||
urlopen('http://httpbin.org/')
|
||||
urlopen('https://httpbin.org/get')
|
||||
assert len(cass) == 2
|
||||
@@ -1,4 +1,3 @@
|
||||
import urllib2
|
||||
import vcr
|
||||
|
||||
|
||||
@@ -11,7 +10,7 @@ class MockSerializer(object):
|
||||
def deserialize(self, cassette_string):
|
||||
self.serialize_count += 1
|
||||
self.cassette_string = cassette_string
|
||||
return ([], [])
|
||||
return {'interactions':[]}
|
||||
|
||||
def serialize(self, cassette_dict):
|
||||
self.deserialize_count += 1
|
||||
@@ -24,7 +23,6 @@ def test_registered_serializer(tmpdir):
|
||||
my_vcr.register_serializer('mock', ms)
|
||||
tmpdir.join('test.mock').write('test_data')
|
||||
with my_vcr.use_cassette(str(tmpdir.join('test.mock')), serializer='mock'):
|
||||
urllib2.urlopen('http://httpbin.org/')
|
||||
# Serializer deserialized once
|
||||
assert ms.serialize_count == 1
|
||||
# and serialized the test data string
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import urllib2
|
||||
import vcr
|
||||
from six.moves.urllib.request import urlopen
|
||||
|
||||
|
||||
def test_recorded_request_url_with_redirected_request(tmpdir):
|
||||
def test_recorded_request_uri_with_redirected_request(tmpdir):
|
||||
with vcr.use_cassette(str(tmpdir.join('test.yml'))) as cass:
|
||||
assert len(cass) == 0
|
||||
urllib2.urlopen('http://httpbin.org/redirect/3')
|
||||
assert cass.requests[0].url == 'http://httpbin.org/redirect/3'
|
||||
assert cass.requests[3].url == 'http://httpbin.org/get'
|
||||
urlopen('http://httpbin.org/redirect/3')
|
||||
assert cass.requests[0].uri == 'http://httpbin.org/redirect/3'
|
||||
assert cass.requests[3].uri == 'http://httpbin.org/get'
|
||||
assert len(cass) == 4
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
import os
|
||||
import pytest
|
||||
import vcr
|
||||
from assertions import assert_cassette_empty, assert_cassette_has_one_response
|
||||
from assertions import (
|
||||
assert_cassette_empty,
|
||||
assert_cassette_has_one_response,
|
||||
assert_is_json
|
||||
)
|
||||
requests = pytest.importorskip("requests")
|
||||
|
||||
|
||||
@@ -21,33 +25,30 @@ def test_status_code(scheme, tmpdir):
|
||||
'''Ensure that we can read the status code'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
|
||||
# Ensure that this is empty to begin with
|
||||
assert_cassette_empty(cass)
|
||||
assert requests.get(url).status_code == requests.get(url).status_code
|
||||
# Ensure that we've now cached a single response
|
||||
assert_cassette_has_one_response(cass)
|
||||
status_code = requests.get(url).status_code
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
|
||||
assert status_code == requests.get(url).status_code
|
||||
|
||||
|
||||
def test_headers(scheme, tmpdir):
|
||||
'''Ensure that we can read the headers back'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
# Ensure that this is empty to begin with
|
||||
assert_cassette_empty(cass)
|
||||
assert requests.get(url).headers == requests.get(url).headers
|
||||
# Ensure that we've now cached a single response
|
||||
assert_cassette_has_one_response(cass)
|
||||
headers = requests.get(url).headers
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
assert headers == requests.get(url).headers
|
||||
|
||||
|
||||
def test_body(tmpdir, scheme):
|
||||
'''Ensure the responses are all identical enough'''
|
||||
url = scheme + '://httpbin.org/bytes/1024'
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
|
||||
# Ensure that this is empty to begin with
|
||||
assert_cassette_empty(cass)
|
||||
assert requests.get(url).content == requests.get(url).content
|
||||
# Ensure that we've now cached a single response
|
||||
assert_cassette_has_one_response(cass)
|
||||
content = requests.get(url).content
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
|
||||
assert content == requests.get(url).content
|
||||
|
||||
|
||||
def test_auth(tmpdir, scheme):
|
||||
@@ -55,14 +56,12 @@ def test_auth(tmpdir, scheme):
|
||||
auth = ('user', 'passwd')
|
||||
url = scheme + '://httpbin.org/basic-auth/user/passwd'
|
||||
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass:
|
||||
# Ensure that this is empty to begin with
|
||||
assert_cassette_empty(cass)
|
||||
one = requests.get(url, auth=auth)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass:
|
||||
two = requests.get(url, auth=auth)
|
||||
assert one.content == two.content
|
||||
assert one.status_code == two.status_code
|
||||
# Ensure that we've now cached a single response
|
||||
assert_cassette_has_one_response(cass)
|
||||
|
||||
|
||||
def test_auth_failed(tmpdir, scheme):
|
||||
@@ -76,31 +75,29 @@ def test_auth_failed(tmpdir, scheme):
|
||||
two = requests.get(url, auth=auth)
|
||||
assert one.content == two.content
|
||||
assert one.status_code == two.status_code == 401
|
||||
# Ensure that we've now cached a single response
|
||||
assert_cassette_has_one_response(cass)
|
||||
|
||||
|
||||
def test_post(tmpdir, scheme):
|
||||
'''Ensure that we can post and cache the results'''
|
||||
data = {'key1': 'value1', 'key2': 'value2'}
|
||||
url = scheme + '://httpbin.org/post'
|
||||
with vcr.use_cassette(str(tmpdir.join('redirect.yaml'))) as cass:
|
||||
# Ensure that this is empty to begin with
|
||||
assert_cassette_empty(cass)
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
|
||||
req1 = requests.post(url, data).content
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
|
||||
req2 = requests.post(url, data).content
|
||||
assert req1 == req2
|
||||
# Ensure that we've now cached a single response
|
||||
assert_cassette_has_one_response(cass)
|
||||
|
||||
assert req1 == req2
|
||||
|
||||
|
||||
def test_redirects(tmpdir, scheme):
|
||||
'''Ensure that we can handle redirects'''
|
||||
url = scheme + '://httpbin.org/redirect-to?url=bytes/1024'
|
||||
with vcr.use_cassette(str(tmpdir.join('redirect.yaml'))) as cass:
|
||||
# Ensure that this is empty to begin with
|
||||
assert_cassette_empty(cass)
|
||||
assert requests.get(url).content == requests.get(url).content
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
|
||||
content = requests.get(url).content
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
|
||||
assert content == requests.get(url).content
|
||||
# Ensure that we've now cached *two* responses. One for the redirect
|
||||
# and one for the final fetch
|
||||
assert len(cass) == 2
|
||||
@@ -117,3 +114,34 @@ def test_cross_scheme(tmpdir, scheme):
|
||||
requests.get('http://httpbin.org/')
|
||||
assert cass.play_count == 0
|
||||
assert len(cass) == 2
|
||||
|
||||
|
||||
def test_gzip(tmpdir, scheme):
|
||||
'''
|
||||
Ensure that requests (actually urllib3) is able to automatically decompress
|
||||
the response body
|
||||
'''
|
||||
url = scheme + '://httpbin.org/gzip'
|
||||
response = requests.get(url)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))) as cass:
|
||||
response = requests.get(url)
|
||||
assert_is_json(response.content)
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('gzip.yaml'))) as cass:
|
||||
assert_is_json(response.content)
|
||||
|
||||
|
||||
def test_session_and_connection_close(tmpdir, scheme):
|
||||
'''
|
||||
This tests the issue in https://github.com/kevin1024/vcrpy/issues/48
|
||||
|
||||
If you use a requests.session and the connection is closed, then an
|
||||
exception is raised in the urllib3 module vendored into requests:
|
||||
`AttributeError: 'NoneType' object has no attribute 'settimeout'`
|
||||
'''
|
||||
with vcr.use_cassette(str(tmpdir.join('session_connection_closed.yaml'))):
|
||||
session = requests.session()
|
||||
|
||||
resp = session.get('http://httpbin.org/get', headers={'Connection': 'close'})
|
||||
resp = session.get('http://httpbin.org/get', headers={'Connection': 'close'})
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
|
||||
# External imports
|
||||
import os
|
||||
import urllib2
|
||||
from urllib import urlencode
|
||||
|
||||
import pytest
|
||||
from six.moves.urllib.request import urlopen
|
||||
from six.moves.urllib_parse import urlencode
|
||||
|
||||
# Internal imports
|
||||
import vcr
|
||||
@@ -25,52 +26,44 @@ def test_response_code(scheme, tmpdir):
|
||||
'''Ensure we can read a response code from a fetch'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
|
||||
# Ensure that this is empty to begin with
|
||||
assert_cassette_empty(cass)
|
||||
assert urllib2.urlopen(url).getcode() == urllib2.urlopen(url).getcode()
|
||||
# Ensure that we've now cached a single response
|
||||
assert_cassette_has_one_response(cass)
|
||||
code = urlopen(url).getcode()
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
|
||||
assert code == urlopen(url).getcode()
|
||||
|
||||
|
||||
def test_random_body(scheme, tmpdir):
|
||||
'''Ensure we can read the content, and that it's served from cache'''
|
||||
url = scheme + '://httpbin.org/bytes/1024'
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
|
||||
# Ensure that this is empty to begin with
|
||||
assert_cassette_empty(cass)
|
||||
assert urllib2.urlopen(url).read() == urllib2.urlopen(url).read()
|
||||
# Ensure that we've now cached a single response
|
||||
assert_cassette_has_one_response(cass)
|
||||
body = urlopen(url).read()
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
|
||||
assert body == urlopen(url).read()
|
||||
|
||||
|
||||
def test_response_headers(scheme, tmpdir):
|
||||
'''Ensure we can get information from the response'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
# Ensure that this is empty to begin with
|
||||
assert_cassette_empty(cass)
|
||||
open1 = urllib2.urlopen(url).info().items()
|
||||
open2 = urllib2.urlopen(url).info().items()
|
||||
assert open1 == open2
|
||||
# Ensure that we've now cached a single response
|
||||
assert_cassette_has_one_response(cass)
|
||||
open1 = urlopen(url).info().items()
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
|
||||
open2 = urlopen(url).info().items()
|
||||
assert sorted(open1) == sorted(open2)
|
||||
|
||||
|
||||
def test_multiple_requests(scheme, tmpdir):
|
||||
'''Ensure that we can cache multiple requests'''
|
||||
urls = [
|
||||
scheme + '://httpbin.org/',
|
||||
scheme + '://httpbin.org/',
|
||||
scheme + '://httpbin.org/get',
|
||||
scheme + '://httpbin.org/bytes/1024'
|
||||
]
|
||||
with vcr.use_cassette(str(tmpdir.join('multiple.yaml'))) as cass:
|
||||
for index in range(len(urls)):
|
||||
url = urls[index]
|
||||
assert len(cass) == index
|
||||
assert cass.play_count == index
|
||||
assert urllib2.urlopen(url).read() == urllib2.urlopen(url).read()
|
||||
assert len(cass) == index + 1
|
||||
assert cass.play_count == index + 1
|
||||
[urlopen(url) for url in urls]
|
||||
assert len(cass) == len(urls)
|
||||
|
||||
|
||||
def test_get_data(scheme, tmpdir):
|
||||
@@ -78,42 +71,38 @@ def test_get_data(scheme, tmpdir):
|
||||
data = urlencode({'some': 1, 'data': 'here'})
|
||||
url = scheme + '://httpbin.org/get?' + data
|
||||
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
|
||||
# Ensure that this is empty to begin with
|
||||
assert_cassette_empty(cass)
|
||||
res1 = urllib2.urlopen(url).read()
|
||||
res2 = urllib2.urlopen(url).read()
|
||||
assert res1 == res2
|
||||
# Ensure that we've now cached a single response
|
||||
assert len(cass) == 1
|
||||
assert cass.play_count == 1
|
||||
res1 = urlopen(url).read()
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
|
||||
res2 = urlopen(url).read()
|
||||
|
||||
assert res1 == res2
|
||||
|
||||
|
||||
def test_post_data(scheme, tmpdir):
|
||||
'''Ensure that it works when posting data'''
|
||||
data = urlencode({'some': 1, 'data': 'here'})
|
||||
data = urlencode({'some': 1, 'data': 'here'}).encode('utf-8')
|
||||
url = scheme + '://httpbin.org/post'
|
||||
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
|
||||
# Ensure that this is empty to begin with
|
||||
assert_cassette_empty(cass)
|
||||
res1 = urllib2.urlopen(url, data).read()
|
||||
res2 = urllib2.urlopen(url, data).read()
|
||||
assert res1 == res2
|
||||
# Ensure that we've now cached a single response
|
||||
assert_cassette_has_one_response(cass)
|
||||
res1 = urlopen(url, data).read()
|
||||
|
||||
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
|
||||
res2 = urlopen(url, data).read()
|
||||
|
||||
assert res1 == res2
|
||||
assert_cassette_has_one_response(cass)
|
||||
|
||||
|
||||
def test_post_unicode_data(scheme, tmpdir):
|
||||
'''Ensure that it works when posting unicode data'''
|
||||
data = urlencode({'snowman': u'☃'.encode('utf-8')})
|
||||
data = urlencode({'snowman': u'☃'.encode('utf-8')}).encode('utf-8')
|
||||
url = scheme + '://httpbin.org/post'
|
||||
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
|
||||
# Ensure that this is empty to begin with
|
||||
assert_cassette_empty(cass)
|
||||
res1 = urllib2.urlopen(url, data).read()
|
||||
res2 = urllib2.urlopen(url, data).read()
|
||||
assert res1 == res2
|
||||
# Ensure that we've now cached a single response
|
||||
assert_cassette_has_one_response(cass)
|
||||
res1 = urlopen(url, data).read()
|
||||
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
|
||||
res2 = urlopen(url, data).read()
|
||||
assert res1 == res2
|
||||
assert_cassette_has_one_response(cass)
|
||||
|
||||
|
||||
def test_cross_scheme(tmpdir):
|
||||
@@ -122,7 +111,21 @@ def test_cross_scheme(tmpdir):
|
||||
# ensure that we haven't served anything out of cache, and we have two
|
||||
# requests / response pairs in the cassette
|
||||
with vcr.use_cassette(str(tmpdir.join('cross_scheme.yaml'))) as cass:
|
||||
urllib2.urlopen('https://httpbin.org/')
|
||||
urllib2.urlopen('http://httpbin.org/')
|
||||
urlopen('https://httpbin.org/')
|
||||
urlopen('http://httpbin.org/')
|
||||
assert len(cass) == 2
|
||||
assert cass.play_count == 0
|
||||
|
||||
def test_decorator(scheme, tmpdir):
|
||||
'''Test the decorator version of VCR.py'''
|
||||
url = scheme + '://httpbin.org/'
|
||||
|
||||
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
|
||||
def inner1():
|
||||
return urlopen(url).getcode()
|
||||
|
||||
@vcr.use_cassette(str(tmpdir.join('atts.yaml')))
|
||||
def inner2():
|
||||
return urlopen(url).getcode()
|
||||
|
||||
assert inner1() == inner2()
|
||||
|
||||
@@ -3,6 +3,11 @@ requests = pytest.importorskip("requests")
|
||||
|
||||
import vcr
|
||||
|
||||
try:
|
||||
import httplib
|
||||
except ImportError:
|
||||
import http.client as httplib
|
||||
|
||||
|
||||
def test_domain_redirect():
|
||||
'''Ensure that redirects across domains are considered unique'''
|
||||
@@ -15,3 +20,47 @@ def test_domain_redirect():
|
||||
# Ensure that we've now served two responses. One for the original
|
||||
# redirect, and a second for the actual fetch
|
||||
assert len(cass) == 2
|
||||
|
||||
|
||||
def test_flickr_multipart_upload():
|
||||
"""
|
||||
The python-flickr-api project does a multipart
|
||||
upload that confuses vcrpy
|
||||
"""
|
||||
def _pretend_to_be_flickr_library():
|
||||
content_type, body = "text/plain", "HELLO WORLD"
|
||||
h = httplib.HTTPConnection("httpbin.org")
|
||||
headers = {
|
||||
"Content-Type": content_type,
|
||||
"content-length": str(len(body))
|
||||
}
|
||||
h.request("POST", "/post/", headers=headers)
|
||||
h.send(body)
|
||||
r = h.getresponse()
|
||||
data = r.read()
|
||||
h.close()
|
||||
|
||||
with vcr.use_cassette('fixtures/vcr_cassettes/flickr.yaml') as cass:
|
||||
_pretend_to_be_flickr_library()
|
||||
assert len(cass) == 1
|
||||
|
||||
with vcr.use_cassette('fixtures/vcr_cassettes/flickr.yaml') as cass:
|
||||
assert len(cass) == 1
|
||||
_pretend_to_be_flickr_library()
|
||||
assert cass.play_count == 1
|
||||
|
||||
|
||||
def test_flickr_should_respond_with_200(tmpdir):
|
||||
testfile = str(tmpdir.join('flickr.yml'))
|
||||
with vcr.use_cassette(testfile):
|
||||
r = requests.post("http://api.flickr.com/services/upload")
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
def test_cookies(tmpdir):
|
||||
testfile = str(tmpdir.join('cookies.yml'))
|
||||
with vcr.use_cassette(testfile):
|
||||
s = requests.Session()
|
||||
r1 = s.get("http://httpbin.org/cookies/set?k1=v1&k2=v2")
|
||||
r2 = s.get("http://httpbin.org/cookies")
|
||||
assert len(r2.json()['cookies']) == 2
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import pytest
|
||||
import yaml
|
||||
import mock
|
||||
from vcr.cassette import Cassette
|
||||
from vcr.errors import UnhandledHTTPRequestError
|
||||
|
||||
|
||||
def test_cassette_load(tmpdir):
|
||||
a_file = tmpdir.join('test_cassette.yml')
|
||||
a_file.write(yaml.dump([
|
||||
{'request': 'foo', 'response': 'bar'}
|
||||
]))
|
||||
a_file.write(yaml.dump({'interactions': [
|
||||
{'request': {'body': '', 'uri': 'foo', 'method': 'GET', 'headers': {}},
|
||||
'response': 'bar'}
|
||||
]}))
|
||||
a_cassette = Cassette.load(str(a_file))
|
||||
assert len(a_cassette) == 1
|
||||
|
||||
@@ -17,21 +20,6 @@ def test_cassette_not_played():
|
||||
assert not a.play_count
|
||||
|
||||
|
||||
def test_cassette_played():
|
||||
a = Cassette('test')
|
||||
a.mark_played('foo')
|
||||
a.mark_played('foo')
|
||||
assert a.play_count == 2
|
||||
|
||||
|
||||
def test_cassette_play_counter():
|
||||
a = Cassette('test')
|
||||
a.mark_played('foo')
|
||||
a.mark_played('bar')
|
||||
assert a.play_counts['foo'] == 1
|
||||
assert a.play_counts['bar'] == 1
|
||||
|
||||
|
||||
def test_cassette_append():
|
||||
a = Cassette('test')
|
||||
a.append('foo', 'bar')
|
||||
@@ -46,19 +34,49 @@ def test_cassette_len():
|
||||
assert len(a) == 2
|
||||
|
||||
|
||||
def _mock_requests_match(request1, request2, matchers):
|
||||
return request1 == request2
|
||||
|
||||
|
||||
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
|
||||
def test_cassette_contains():
|
||||
a = Cassette('test')
|
||||
a.append('foo', 'bar')
|
||||
assert 'foo' in a
|
||||
|
||||
|
||||
def test_cassette_response_of():
|
||||
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
|
||||
def test_cassette_responses_of():
|
||||
a = Cassette('test')
|
||||
a.append('foo', 'bar')
|
||||
assert a.response_of('foo') == 'bar'
|
||||
assert a.responses_of('foo') == ['bar']
|
||||
|
||||
|
||||
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
|
||||
def test_cassette_get_missing_response():
|
||||
a = Cassette('test')
|
||||
with pytest.raises(KeyError):
|
||||
a.response_of('foo')
|
||||
with pytest.raises(UnhandledHTTPRequestError):
|
||||
a.responses_of('foo')
|
||||
|
||||
|
||||
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
|
||||
def test_cassette_cant_read_same_request_twice():
|
||||
a = Cassette('test')
|
||||
a.append('foo', 'bar')
|
||||
a.play_response('foo')
|
||||
with pytest.raises(UnhandledHTTPRequestError):
|
||||
a.play_response('foo')
|
||||
|
||||
|
||||
def test_cassette_not_all_played():
|
||||
a = Cassette('test')
|
||||
a.append('foo', 'bar')
|
||||
assert not a.all_played
|
||||
|
||||
|
||||
@mock.patch('vcr.cassette.requests_match', _mock_requests_match)
|
||||
def test_cassette_all_played():
|
||||
a = Cassette('test')
|
||||
a.append('foo', 'bar')
|
||||
a.play_response('foo')
|
||||
assert a.all_played
|
||||
|
||||
37
tests/unit/test_filters.py
Normal file
37
tests/unit/test_filters.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from vcr.filters import _remove_headers, _remove_query_parameters
|
||||
from vcr.request import Request
|
||||
|
||||
|
||||
def test_remove_headers():
|
||||
headers = {'hello': ['goodbye'], 'secret': ['header']}
|
||||
request = Request('GET', 'http://google.com', '', headers)
|
||||
_remove_headers(request, ['secret'])
|
||||
assert request.headers == {'hello': 'goodbye'}
|
||||
|
||||
|
||||
def test_remove_headers_empty():
|
||||
headers = {'hello': 'goodbye', 'secret': 'header'}
|
||||
request = Request('GET', 'http://google.com', '', headers)
|
||||
_remove_headers(request, [])
|
||||
assert request.headers == headers
|
||||
|
||||
|
||||
def test_remove_query_parameters():
|
||||
uri = 'http://g.com/?q=cowboys&w=1'
|
||||
request = Request('GET', uri, '', {})
|
||||
_remove_query_parameters(request, ['w'])
|
||||
assert request.uri == 'http://g.com/?q=cowboys'
|
||||
|
||||
|
||||
def test_remove_all_query_parameters():
|
||||
uri = 'http://g.com/?q=cowboys&w=1'
|
||||
request = Request('GET', uri, '', {})
|
||||
_remove_query_parameters(request, ['w', 'q'])
|
||||
assert request.uri == 'http://g.com/'
|
||||
|
||||
|
||||
def test_remove_nonexistent_query_parameters():
|
||||
uri = 'http://g.com/'
|
||||
request = Request('GET', uri, '', {})
|
||||
_remove_query_parameters(request, ['w', 'q'])
|
||||
assert request.uri == 'http://g.com/'
|
||||
19
tests/unit/test_json_serializer.py
Normal file
19
tests/unit/test_json_serializer.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import pytest
|
||||
from vcr.serializers.jsonserializer import serialize
|
||||
from vcr.request import Request
|
||||
|
||||
|
||||
def test_serialize_binary():
|
||||
request = Request(
|
||||
method='GET',
|
||||
uri='http://localhost/',
|
||||
body='',
|
||||
headers={},
|
||||
)
|
||||
cassette = {'requests': [request], 'responses': [{'body': b'\x8c'}]}
|
||||
|
||||
with pytest.raises(Exception) as e:
|
||||
serialize(cassette)
|
||||
assert e.message == "Error serializing cassette to JSON. Does this \
|
||||
HTTP interaction contain binary data? If so, use a different \
|
||||
serializer (like the yaml serializer) for this request"
|
||||
56
tests/unit/test_matchers.py
Normal file
56
tests/unit/test_matchers.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import itertools
|
||||
|
||||
from vcr import matchers
|
||||
from vcr import request
|
||||
|
||||
# the dict contains requests with corresponding to its key difference
|
||||
# with 'base' request.
|
||||
REQUESTS = {
|
||||
'base': request.Request('GET', 'http://host.com/p?a=b', '', {}),
|
||||
'method': request.Request('POST', 'http://host.com/p?a=b', '', {}),
|
||||
'scheme': request.Request('GET', 'https://host.com:80/p?a=b', '', {}),
|
||||
'host': request.Request('GET', 'http://another-host.com/p?a=b', '', {}),
|
||||
'port': request.Request('GET', 'http://host.com:90/p?a=b', '', {}),
|
||||
'path': request.Request('GET', 'http://host.com/x?a=b', '', {}),
|
||||
'query': request.Request('GET', 'http://host.com/p?c=d', '', {}),
|
||||
}
|
||||
|
||||
|
||||
def assert_matcher(matcher_name):
|
||||
matcher = getattr(matchers, matcher_name)
|
||||
for k1, k2 in itertools.permutations(REQUESTS, 2):
|
||||
matched = matcher(REQUESTS[k1], REQUESTS[k2])
|
||||
if matcher_name in set((k1, k2)):
|
||||
assert not matched
|
||||
else:
|
||||
assert matched
|
||||
|
||||
|
||||
def test_uri_matcher():
|
||||
for k1, k2 in itertools.permutations(REQUESTS, 2):
|
||||
matched = matchers.uri(REQUESTS[k1], REQUESTS[k2])
|
||||
if set((k1, k2)) != set(('base', 'method')):
|
||||
assert not matched
|
||||
else:
|
||||
assert matched
|
||||
|
||||
|
||||
def test_query_matcher():
|
||||
req1 = request.Request('GET', 'http://host.com/?a=b&c=d', '', {})
|
||||
req2 = request.Request('GET', 'http://host.com/?c=d&a=b', '', {})
|
||||
assert matchers.query(req1, req2)
|
||||
|
||||
req1 = request.Request('GET', 'http://host.com/?a=b&a=b&c=d', '', {})
|
||||
req2 = request.Request('GET', 'http://host.com/?a=b&c=d&a=b', '', {})
|
||||
req3 = request.Request('GET', 'http://host.com/?c=d&a=b&a=b', '', {})
|
||||
assert matchers.query(req1, req2)
|
||||
assert matchers.query(req1, req3)
|
||||
|
||||
|
||||
def test_metchers():
|
||||
assert_matcher('method')
|
||||
assert_matcher('scheme')
|
||||
assert_matcher('host')
|
||||
assert_matcher('port')
|
||||
assert_matcher('path')
|
||||
assert_matcher('query')
|
||||
41
tests/unit/test_migration.py
Normal file
41
tests/unit/test_migration.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import filecmp
|
||||
import json
|
||||
import shutil
|
||||
import yaml
|
||||
|
||||
import vcr.migration
|
||||
|
||||
|
||||
def test_try_migrate_with_json(tmpdir):
|
||||
cassette = tmpdir.join('cassette.json').strpath
|
||||
shutil.copy('tests/fixtures/migration/old_cassette.json', cassette)
|
||||
assert vcr.migration.try_migrate(cassette)
|
||||
with open('tests/fixtures/migration/new_cassette.json', 'r') as f:
|
||||
expected_json = json.load(f)
|
||||
with open(cassette, 'r') as f:
|
||||
actual_json = json.load(f)
|
||||
assert actual_json == expected_json
|
||||
|
||||
|
||||
def test_try_migrate_with_yaml(tmpdir):
|
||||
cassette = tmpdir.join('cassette.yaml').strpath
|
||||
shutil.copy('tests/fixtures/migration/old_cassette.yaml', cassette)
|
||||
assert vcr.migration.try_migrate(cassette)
|
||||
with open('tests/fixtures/migration/new_cassette.yaml', 'r') as f:
|
||||
expected_yaml = yaml.load(f)
|
||||
with open(cassette, 'r') as f:
|
||||
actual_yaml = yaml.load(f)
|
||||
assert actual_yaml == expected_yaml
|
||||
|
||||
|
||||
def test_try_migrate_with_invalid_or_new_cassettes(tmpdir):
|
||||
cassette = tmpdir.join('cassette').strpath
|
||||
files = [
|
||||
'tests/fixtures/migration/not_cassette.txt',
|
||||
'tests/fixtures/migration/new_cassette.yaml',
|
||||
'tests/fixtures/migration/new_cassette.json',
|
||||
]
|
||||
for file_path in files:
|
||||
shutil.copy(file_path, cassette)
|
||||
assert not vcr.migration.try_migrate(cassette)
|
||||
assert filecmp.cmp(cassette, file_path) # shold not change file
|
||||
24
tests/unit/test_persist.py
Normal file
24
tests/unit/test_persist.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
|
||||
import vcr.persist
|
||||
from vcr.serializers import jsonserializer, yamlserializer
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cassette_path, serializer", [
|
||||
('tests/fixtures/migration/old_cassette.json', jsonserializer),
|
||||
('tests/fixtures/migration/old_cassette.yaml', yamlserializer),
|
||||
])
|
||||
def test_load_cassette_with_old_cassettes(cassette_path, serializer):
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
vcr.persist.load_cassette(cassette_path, serializer)
|
||||
assert "run the migration script" in excinfo.exconly()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cassette_path, serializer", [
|
||||
('tests/fixtures/migration/not_cassette.txt', jsonserializer),
|
||||
('tests/fixtures/migration/not_cassette.txt', yamlserializer),
|
||||
])
|
||||
def test_load_cassette_with_invalid_cassettes(cassette_path, serializer):
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
vcr.persist.load_cassette(cassette_path, serializer)
|
||||
assert "run the migration script" not in excinfo.exconly()
|
||||
@@ -1,11 +1,38 @@
|
||||
import pytest
|
||||
|
||||
from vcr.request import Request
|
||||
|
||||
|
||||
def test_url():
|
||||
req = Request('http', 'www.google.com', 80, 'GET', '/', '', {})
|
||||
assert req.url == 'http://www.google.com/'
|
||||
|
||||
|
||||
def test_str():
|
||||
req = Request('http', 'www.google.com', 80, 'GET', '/', '', {})
|
||||
str(req) == '<Request (GET) http://www.google.com>'
|
||||
req = Request('GET', 'http://www.google.com/', '', {})
|
||||
str(req) == '<Request (GET) http://www.google.com/>'
|
||||
|
||||
|
||||
def test_headers():
|
||||
headers = {'X-Header1': ['h1'], 'X-Header2': 'h2'}
|
||||
req = Request('GET', 'http://go.com/', '', headers)
|
||||
assert req.headers == {'X-Header1': 'h1', 'X-Header2': 'h2'}
|
||||
|
||||
req.add_header('X-Header1', 'h11')
|
||||
assert req.headers == {'X-Header1': 'h11', 'X-Header2': 'h2'}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("uri, expected_port", [
|
||||
('http://go.com/', 80),
|
||||
('http://go.com:80/', 80),
|
||||
('http://go.com:3000/', 3000),
|
||||
('https://go.com/', 433),
|
||||
('https://go.com:433/', 433),
|
||||
('https://go.com:3000/', 3000),
|
||||
])
|
||||
def test_port(uri, expected_port):
|
||||
req = Request('GET', uri, '', {})
|
||||
assert req.port == expected_port
|
||||
|
||||
|
||||
def test_uri():
|
||||
req = Request('GET', 'http://go.com/', '', {})
|
||||
assert req.uri == 'http://go.com/'
|
||||
|
||||
req = Request('GET', 'http://go.com:80/', '', {})
|
||||
assert req.uri == 'http://go.com:80/'
|
||||
|
||||
21
tests/unit/test_serialize.py
Normal file
21
tests/unit/test_serialize.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import pytest
|
||||
from vcr.serialize import deserialize
|
||||
from vcr.serializers import yamlserializer, jsonserializer
|
||||
|
||||
def test_deserialize_old_yaml_cassette():
|
||||
with open('tests/fixtures/migration/old_cassette.yaml', 'r') as f:
|
||||
with pytest.raises(ValueError):
|
||||
deserialize(f.read(), yamlserializer)
|
||||
|
||||
def test_deserialize_old_json_cassette():
|
||||
with open('tests/fixtures/migration/old_cassette.json', 'r') as f:
|
||||
with pytest.raises(ValueError):
|
||||
deserialize(f.read(), jsonserializer)
|
||||
|
||||
def test_deserialize_new_yaml_cassette():
|
||||
with open('tests/fixtures/migration/new_cassette.yaml', 'r') as f:
|
||||
deserialize(f.read(), yamlserializer)
|
||||
|
||||
def test_deserialize_new_json_cassette():
|
||||
with open('tests/fixtures/migration/new_cassette.json', 'r') as f:
|
||||
deserialize(f.read(), jsonserializer)
|
||||
190
tox.ini
190
tox.ini
@@ -4,32 +4,206 @@
|
||||
# and then run "tox" from this directory.
|
||||
|
||||
[tox]
|
||||
envlist = py26, py27, pypy, py26requests, py27requests, pypyrequests
|
||||
envlist =
|
||||
py26,
|
||||
py27,
|
||||
py33,
|
||||
py34,
|
||||
pypy,
|
||||
py26requests23,
|
||||
py27requests23,
|
||||
py34requests23,
|
||||
pypyrequests23,
|
||||
py26requests22,
|
||||
py27requests22,
|
||||
py34requests22,
|
||||
pypyrequests22,
|
||||
py26requests1,
|
||||
py27requests1,
|
||||
py33requests1,
|
||||
pypyrequests1,
|
||||
py26httplib2,
|
||||
py27httplib2,
|
||||
py33httplib2,
|
||||
py34httplib2,
|
||||
pypyhttplib2,
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
python setup.py test
|
||||
py.test {posargs}
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
|
||||
[testenv:py26requests]
|
||||
[testenv:py26requests1]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests
|
||||
requests==1.2.3
|
||||
|
||||
[testenv:py27requests]
|
||||
[testenv:py27requests1]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests
|
||||
requests==1.2.3
|
||||
|
||||
[testenv:pypyrequests]
|
||||
[testenv:py33requests1]
|
||||
basepython = python3.3
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests==1.2.3
|
||||
|
||||
[testenv:pypyrequests1]
|
||||
basepython = pypy
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests
|
||||
requests==1.2.3
|
||||
|
||||
[testenv:py26requests23]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests==2.3.0
|
||||
|
||||
[testenv:py27requests23]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests==2.3.0
|
||||
|
||||
[testenv:py33requests23]
|
||||
basepython = python3.4
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests==2.3.0
|
||||
|
||||
[testenv:py34requests23]
|
||||
basepython = python3.4
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests==2.3.0
|
||||
|
||||
[testenv:pypyrequests23]
|
||||
basepython = pypy
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests==2.3.0
|
||||
|
||||
[testenv:py26requests22]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests==2.2.1
|
||||
|
||||
[testenv:py27requests22]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests==2.2.1
|
||||
|
||||
[testenv:py33requests22]
|
||||
basepython = python3.4
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests==2.2.1
|
||||
|
||||
[testenv:py34requests22]
|
||||
basepython = python3.4
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests==2.2.1
|
||||
|
||||
[testenv:pypyrequests22]
|
||||
basepython = pypy
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
requests==2.2.1
|
||||
|
||||
[testenv:py26httplib2]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
httplib2
|
||||
|
||||
[testenv:py27httplib2]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
httplib2
|
||||
|
||||
[testenv:py33httplib2]
|
||||
basepython = python3.4
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
httplib2
|
||||
|
||||
[testenv:py34httplib2]
|
||||
basepython = python3.4
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
httplib2
|
||||
|
||||
[testenv:pypyhttplib2]
|
||||
basepython = pypy
|
||||
deps =
|
||||
mock
|
||||
pytest
|
||||
pytest-localserver
|
||||
PyYAML
|
||||
httplib2
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
from config import VCR
|
||||
import logging
|
||||
from .config import VCR
|
||||
|
||||
# Set default logging handler to avoid "No handler found" warnings.
|
||||
import logging
|
||||
try: # Python 2.7+
|
||||
from logging import NullHandler
|
||||
except ImportError:
|
||||
class NullHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
logging.getLogger(__name__).addHandler(NullHandler())
|
||||
|
||||
default_vcr = VCR()
|
||||
|
||||
|
||||
# Also, make a 'load' function available
|
||||
def use_cassette(path, **kwargs):
|
||||
return default_vcr.use_cassette(path, **kwargs)
|
||||
|
||||
128
vcr/cassette.py
128
vcr/cassette.py
@@ -1,19 +1,24 @@
|
||||
'''The container for recorded requests and responses'''
|
||||
|
||||
try:
|
||||
from collections import Counter, OrderedDict
|
||||
from collections import Counter
|
||||
except ImportError:
|
||||
from .compat.counter import Counter
|
||||
from .compat.ordereddict import OrderedDict
|
||||
|
||||
from contextdecorator import ContextDecorator
|
||||
|
||||
# Internal imports
|
||||
from .patch import install, reset
|
||||
from .persist import load_cassette, save_cassette
|
||||
from .filters import filter_request
|
||||
from .serializers import yamlserializer
|
||||
from .matchers import requests_match, uri, method
|
||||
from .errors import UnhandledHTTPRequestError
|
||||
|
||||
|
||||
class Cassette(object):
|
||||
class Cassette(ContextDecorator):
|
||||
'''A container for recorded requests and responses'''
|
||||
|
||||
@classmethod
|
||||
def load(cls, path, **kwargs):
|
||||
'''Load in the cassette stored at the provided path'''
|
||||
@@ -21,39 +26,125 @@ class Cassette(object):
|
||||
new_cassette._load()
|
||||
return new_cassette
|
||||
|
||||
def __init__(self, path, serializer=yamlserializer):
|
||||
def __init__(self,
|
||||
path,
|
||||
serializer=yamlserializer,
|
||||
record_mode='once',
|
||||
match_on=[uri, method],
|
||||
filter_headers=[],
|
||||
filter_query_parameters=[],
|
||||
before_record=None,
|
||||
ignore_hosts=[],
|
||||
ignore_localhost=[],
|
||||
):
|
||||
self._path = path
|
||||
self._serializer = serializer
|
||||
self.data = OrderedDict()
|
||||
self._match_on = match_on
|
||||
self._filter_headers = filter_headers
|
||||
self._filter_query_parameters = filter_query_parameters
|
||||
self._before_record = before_record
|
||||
self._ignore_hosts = ignore_hosts
|
||||
if ignore_localhost:
|
||||
self._ignore_hosts = list(set(
|
||||
self._ignore_hosts + ['localhost', '0.0.0.0', '127.0.0.1']
|
||||
))
|
||||
|
||||
# self.data is the list of (req, resp) tuples
|
||||
self.data = []
|
||||
self.play_counts = Counter()
|
||||
self.dirty = False
|
||||
self.rewound = False
|
||||
self.record_mode = record_mode
|
||||
|
||||
@property
|
||||
def play_count(self):
|
||||
return sum(self.play_counts.values())
|
||||
|
||||
@property
|
||||
def all_played(self):
|
||||
"""
|
||||
Returns True if all responses have been played, False otherwise.
|
||||
"""
|
||||
return self.play_count == len(self)
|
||||
|
||||
@property
|
||||
def requests(self):
|
||||
return self.data.keys()
|
||||
return [request for (request, response) in self.data]
|
||||
|
||||
@property
|
||||
def responses(self):
|
||||
return self.data.values()
|
||||
return [response for (request, response) in self.data]
|
||||
|
||||
def mark_played(self, request):
|
||||
'''
|
||||
Alert the cassette of a request that's been played
|
||||
'''
|
||||
self.play_counts[request] += 1
|
||||
@property
|
||||
def write_protected(self):
|
||||
return self.rewound and self.record_mode == 'once' or \
|
||||
self.record_mode == 'none'
|
||||
|
||||
def _filter_request(self, request):
|
||||
return filter_request(
|
||||
request=request,
|
||||
filter_headers=self._filter_headers,
|
||||
filter_query_parameters=self._filter_query_parameters,
|
||||
before_record=self._before_record,
|
||||
ignore_hosts=self._ignore_hosts
|
||||
)
|
||||
|
||||
def append(self, request, response):
|
||||
'''Add a request, response pair to this cassette'''
|
||||
self.data[request] = response
|
||||
request = self._filter_request(request)
|
||||
if not request:
|
||||
return
|
||||
self.data.append((request, response))
|
||||
self.dirty = True
|
||||
|
||||
def response_of(self, request):
|
||||
'''Find the response corresponding to a request'''
|
||||
return self.data[request]
|
||||
def _responses(self, request):
|
||||
"""
|
||||
internal API, returns an iterator with all responses matching
|
||||
the request.
|
||||
"""
|
||||
request = self._filter_request(request)
|
||||
if not request:
|
||||
return
|
||||
for index, (stored_request, response) in enumerate(self.data):
|
||||
if requests_match(request, stored_request, self._match_on):
|
||||
yield index, response
|
||||
|
||||
def can_play_response_for(self, request):
|
||||
request = self._filter_request(request)
|
||||
return request and request in self and \
|
||||
self.record_mode != 'all' and \
|
||||
self.rewound
|
||||
|
||||
def play_response(self, request):
|
||||
'''
|
||||
Get the response corresponding to a request, but only if it
|
||||
hasn't been played back before, and mark it as played
|
||||
'''
|
||||
for index, response in self._responses(request):
|
||||
if self.play_counts[index] == 0:
|
||||
self.play_counts[index] += 1
|
||||
return response
|
||||
# The cassette doesn't contain the request asked for.
|
||||
raise UnhandledHTTPRequestError(
|
||||
"The cassette (%r) doesn't contain the request (%r) asked for"
|
||||
% (self._path, request)
|
||||
)
|
||||
|
||||
def responses_of(self, request):
|
||||
'''
|
||||
Find the responses corresponding to a request.
|
||||
This function isn't actually used by VCR internally, but is
|
||||
provided as an external API.
|
||||
'''
|
||||
responses = [response for index, response in self._responses(request)]
|
||||
|
||||
if responses:
|
||||
return responses
|
||||
# The cassette doesn't contain the request asked for.
|
||||
raise UnhandledHTTPRequestError(
|
||||
"The cassette (%r) doesn't contain the request (%r) asked for"
|
||||
% (self._path, request)
|
||||
)
|
||||
|
||||
def _as_dict(self):
|
||||
return {"requests": self.requests, "responses": self.responses}
|
||||
@@ -76,6 +167,7 @@ class Cassette(object):
|
||||
for request, response in zip(requests, responses):
|
||||
self.append(request, response)
|
||||
self.dirty = False
|
||||
self.rewound = True
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
@@ -90,7 +182,9 @@ class Cassette(object):
|
||||
|
||||
def __contains__(self, request):
|
||||
'''Return whether or not a request has been stored'''
|
||||
return request in self.data
|
||||
for response in self._responses(request):
|
||||
return True
|
||||
return False
|
||||
|
||||
def __enter__(self):
|
||||
'''Patch the fetching libraries we know about'''
|
||||
|
||||
@@ -1,29 +1,79 @@
|
||||
import os
|
||||
from .cassette import Cassette
|
||||
from .serializers import yamlserializer, jsonserializer
|
||||
from . import matchers
|
||||
|
||||
|
||||
class VCR(object):
|
||||
def __init__(self, serializer='yaml', cassette_library_dir=None):
|
||||
def __init__(self,
|
||||
serializer='yaml',
|
||||
cassette_library_dir=None,
|
||||
record_mode="once",
|
||||
filter_headers=[],
|
||||
filter_query_parameters=[],
|
||||
before_record=None,
|
||||
match_on=[
|
||||
'method',
|
||||
'scheme',
|
||||
'host',
|
||||
'port',
|
||||
'path',
|
||||
'query',
|
||||
],
|
||||
ignore_hosts=[],
|
||||
ignore_localhost=False,
|
||||
):
|
||||
self.serializer = serializer
|
||||
self.match_on = match_on
|
||||
self.cassette_library_dir = cassette_library_dir
|
||||
self.serializers = {
|
||||
'yaml': yamlserializer,
|
||||
'json': jsonserializer,
|
||||
}
|
||||
self.matchers = {
|
||||
'method': matchers.method,
|
||||
'uri': matchers.uri,
|
||||
'url': matchers.uri, # matcher for backwards compatibility
|
||||
'scheme': matchers.scheme,
|
||||
'host': matchers.host,
|
||||
'port': matchers.port,
|
||||
'path': matchers.path,
|
||||
'query': matchers.query,
|
||||
'headers': matchers.headers,
|
||||
'body': matchers.body,
|
||||
}
|
||||
self.record_mode = record_mode
|
||||
self.filter_headers = filter_headers
|
||||
self.filter_query_parameters = filter_query_parameters
|
||||
self.before_record = before_record
|
||||
self.ignore_hosts = ignore_hosts
|
||||
self.ignore_localhost = ignore_localhost
|
||||
|
||||
def _get_serializer(self, serializer_name):
|
||||
try:
|
||||
serializer = self.serializers[serializer_name]
|
||||
except KeyError:
|
||||
print "Serializer {0} doesn't exist or isn't registered".format(
|
||||
print("Serializer {0} doesn't exist or isn't registered".format(
|
||||
serializer_name
|
||||
)
|
||||
))
|
||||
raise KeyError
|
||||
return serializer
|
||||
|
||||
def _get_matchers(self, matcher_names):
|
||||
matchers = []
|
||||
try:
|
||||
for m in matcher_names:
|
||||
matchers.append(self.matchers[m])
|
||||
except KeyError:
|
||||
raise KeyError(
|
||||
"Matcher {0} doesn't exist or isn't registered".format(
|
||||
m)
|
||||
)
|
||||
return matchers
|
||||
|
||||
def use_cassette(self, path, **kwargs):
|
||||
serializer_name = kwargs.get('serializer', self.serializer)
|
||||
matcher_names = kwargs.get('match_on', self.match_on)
|
||||
cassette_library_dir = kwargs.get(
|
||||
'cassette_library_dir',
|
||||
self.cassette_library_dir
|
||||
@@ -34,9 +84,29 @@ class VCR(object):
|
||||
|
||||
merged_config = {
|
||||
"serializer": self._get_serializer(serializer_name),
|
||||
"match_on": self._get_matchers(matcher_names),
|
||||
"record_mode": kwargs.get('record_mode', self.record_mode),
|
||||
"filter_headers": kwargs.get(
|
||||
'filter_headers', self.filter_headers
|
||||
),
|
||||
"filter_query_parameters": kwargs.get(
|
||||
'filter_query_parameters', self.filter_query_parameters
|
||||
),
|
||||
"before_record": kwargs.get(
|
||||
"before_record", self.before_record
|
||||
),
|
||||
"ignore_hosts": kwargs.get(
|
||||
'ignore_hosts', self.ignore_hosts
|
||||
),
|
||||
"ignore_localhost": kwargs.get(
|
||||
'ignore_localhost', self.ignore_localhost
|
||||
),
|
||||
}
|
||||
|
||||
return Cassette.load(path, **merged_config)
|
||||
|
||||
def register_serializer(self, name, serializer):
|
||||
self.serializers[name] = serializer
|
||||
|
||||
def register_matcher(self, name, matcher):
|
||||
self.matchers[name] = matcher
|
||||
|
||||
10
vcr/errors.py
Normal file
10
vcr/errors.py
Normal file
@@ -0,0 +1,10 @@
|
||||
class CannotOverwriteExistingCassetteException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnhandledHTTPRequestError(KeyError):
|
||||
'''
|
||||
Raised when a cassette does not c
|
||||
ontain the request we want
|
||||
'''
|
||||
pass
|
||||
43
vcr/filters.py
Normal file
43
vcr/filters.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from six.moves.urllib.parse import urlparse, urlencode, urlunparse
|
||||
import copy
|
||||
|
||||
|
||||
def _remove_headers(request, headers_to_remove):
|
||||
headers = copy.copy(request.headers)
|
||||
headers_to_remove = [h.lower() for h in headers_to_remove]
|
||||
keys = [k for k in headers if k.lower() in headers_to_remove]
|
||||
if keys:
|
||||
for k in keys:
|
||||
headers.pop(k)
|
||||
request.headers = headers
|
||||
return request
|
||||
|
||||
|
||||
def _remove_query_parameters(request, query_parameters_to_remove):
|
||||
query = request.query
|
||||
new_query = [(k, v) for (k, v) in query
|
||||
if k not in query_parameters_to_remove]
|
||||
if len(new_query) != len(query):
|
||||
uri_parts = list(urlparse(request.uri))
|
||||
uri_parts[4] = urlencode(new_query)
|
||||
request.uri = urlunparse(uri_parts)
|
||||
return request
|
||||
|
||||
|
||||
def filter_request(
|
||||
request,
|
||||
filter_headers,
|
||||
filter_query_parameters,
|
||||
before_record,
|
||||
ignore_hosts
|
||||
):
|
||||
request = copy.copy(request) # don't mutate request object
|
||||
if hasattr(request, 'headers') and filter_headers:
|
||||
request = _remove_headers(request, filter_headers)
|
||||
if hasattr(request, 'host') and request.host in ignore_hosts:
|
||||
return None
|
||||
if filter_query_parameters:
|
||||
request = _remove_query_parameters(request, filter_query_parameters)
|
||||
if before_record:
|
||||
request = before_record(request)
|
||||
return request
|
||||
53
vcr/matchers.py
Normal file
53
vcr/matchers.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def method(r1, r2):
|
||||
return r1.method == r2.method
|
||||
|
||||
|
||||
def uri(r1, r2):
|
||||
return r1.uri == r2.uri
|
||||
|
||||
|
||||
def host(r1, r2):
|
||||
return r1.host == r2.host
|
||||
|
||||
|
||||
def scheme(r1, r2):
|
||||
return r1.scheme == r2.scheme
|
||||
|
||||
|
||||
def port(r1, r2):
|
||||
return r1.port == r2.port
|
||||
|
||||
|
||||
def path(r1, r2):
|
||||
return r1.path == r2.path
|
||||
|
||||
|
||||
def query(r1, r2):
|
||||
return r1.query == r2.query
|
||||
|
||||
|
||||
def body(r1, r2):
|
||||
return r1.body == r2.body
|
||||
|
||||
|
||||
def headers(r1, r2):
|
||||
return r1.headers == r2.headers
|
||||
|
||||
|
||||
def _log_matches(matches):
|
||||
differences = [m for m in matches if not m[0]]
|
||||
if differences:
|
||||
log.debug(
|
||||
'Requests differ according to the following matchers: ' +
|
||||
str(differences)
|
||||
)
|
||||
|
||||
|
||||
def requests_match(r1, r2, matchers):
|
||||
matches = [(m(r1, r2), m) for m in matchers]
|
||||
_log_matches(matches)
|
||||
return all([m[0] for m in matches])
|
||||
168
vcr/migration.py
Normal file
168
vcr/migration.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
Migration script for old 'yaml' and 'json' cassettes
|
||||
|
||||
.. warning:: Backup your cassettes files before migration.
|
||||
|
||||
It merges and deletes the request obsolete keys (protocol, host, port, path)
|
||||
into new 'uri' key.
|
||||
Usage::
|
||||
|
||||
python -m vcr.migration PATH
|
||||
|
||||
The PATH can be path to the directory with cassettes or cassette itself
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import yaml
|
||||
|
||||
from .serializers import compat, yamlserializer, jsonserializer
|
||||
from .serialize import serialize
|
||||
from . import request
|
||||
from .stubs.compat import get_httpmessage
|
||||
|
||||
# Use the libYAML versions if possible
|
||||
try:
|
||||
from yaml import CLoader as Loader
|
||||
except ImportError:
|
||||
from yaml import Loader
|
||||
|
||||
|
||||
def preprocess_yaml(cassette):
|
||||
# this is the hack that makes the whole thing work. The old version used
|
||||
# to deserialize to Request objects automatically using pyYaml's !!python
|
||||
# tag system. This made it difficult to deserialize old cassettes on new
|
||||
# versions. So this just strips the tags before deserializing.
|
||||
|
||||
STRINGS_TO_NUKE = [
|
||||
'!!python/object:vcr.request.Request',
|
||||
'!!python/object/apply:__builtin__.frozenset',
|
||||
'!!python/object/apply:builtins.frozenset',
|
||||
]
|
||||
for s in STRINGS_TO_NUKE:
|
||||
cassette = cassette.replace(s, '')
|
||||
return cassette
|
||||
|
||||
|
||||
PARTS = [
|
||||
'protocol',
|
||||
'host',
|
||||
'port',
|
||||
'path',
|
||||
]
|
||||
|
||||
|
||||
def build_uri(**parts):
|
||||
port = parts['port']
|
||||
scheme = parts['protocol']
|
||||
default_port = {'https': 433, 'http': 80}[scheme]
|
||||
parts['port'] = ':{0}'.format(port) if port != default_port else ''
|
||||
return "{protocol}://{host}{port}{path}".format(**parts)
|
||||
|
||||
|
||||
def _migrate(data):
|
||||
interactions = []
|
||||
for item in data:
|
||||
req = item['request']
|
||||
res = item['response']
|
||||
uri = dict((k, req.pop(k)) for k in PARTS)
|
||||
req['uri'] = build_uri(**uri)
|
||||
# convert headers to dict of lists
|
||||
headers = req['headers']
|
||||
for k in headers:
|
||||
headers[k] = [headers[k]]
|
||||
response_headers = {}
|
||||
for k, v in get_httpmessage(
|
||||
b"".join(h.encode('utf-8') for h in res['headers'])
|
||||
).items():
|
||||
response_headers.setdefault(k, [])
|
||||
response_headers[k].append(v)
|
||||
res['headers'] = response_headers
|
||||
interactions.append({'request': req, 'response': res})
|
||||
return {
|
||||
'requests': [
|
||||
request.Request._from_dict(i['request']) for i in interactions
|
||||
],
|
||||
'responses': [i['response'] for i in interactions],
|
||||
}
|
||||
|
||||
|
||||
def migrate_json(in_fp, out_fp):
|
||||
data = json.load(in_fp)
|
||||
if _already_migrated(data):
|
||||
return False
|
||||
interactions = _migrate(data)
|
||||
out_fp.write(serialize(interactions, jsonserializer))
|
||||
return True
|
||||
|
||||
|
||||
def _list_of_tuples_to_dict(fs):
|
||||
return dict((k, v) for k, v in fs[0])
|
||||
|
||||
|
||||
def _already_migrated(data):
|
||||
try:
|
||||
if data.get('version') == 1:
|
||||
return True
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
|
||||
def migrate_yml(in_fp, out_fp):
|
||||
data = yaml.load(preprocess_yaml(in_fp.read()), Loader=Loader)
|
||||
if _already_migrated(data):
|
||||
return False
|
||||
for i in range(len(data)):
|
||||
data[i]['request']['headers'] = _list_of_tuples_to_dict(
|
||||
data[i]['request']['headers']
|
||||
)
|
||||
interactions = _migrate(data)
|
||||
out_fp.write(serialize(interactions, yamlserializer))
|
||||
return True
|
||||
|
||||
|
||||
def migrate(file_path, migration_fn):
|
||||
# because we assume that original files can be reverted
|
||||
# we will try to copy the content. (os.rename not needed)
|
||||
with tempfile.TemporaryFile(mode='w+') as out_fp:
|
||||
with open(file_path, 'r') as in_fp:
|
||||
if not migration_fn(in_fp, out_fp):
|
||||
return False
|
||||
with open(file_path, 'w') as in_fp:
|
||||
out_fp.seek(0)
|
||||
shutil.copyfileobj(out_fp, in_fp)
|
||||
return True
|
||||
|
||||
|
||||
def try_migrate(path):
|
||||
if path.endswith('.json'):
|
||||
return migrate(path, migrate_json)
|
||||
elif path.endswith('.yaml') or path.endswith('.yml'):
|
||||
return migrate(path, migrate_yml)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
raise SystemExit("Please provide path to cassettes directory or file. "
|
||||
"Usage: python -m vcr.migration PATH")
|
||||
|
||||
path = sys.argv[1]
|
||||
if not os.path.isabs(path):
|
||||
path = os.path.abspath(path)
|
||||
files = [path]
|
||||
if os.path.isdir(path):
|
||||
files = (os.path.join(root, name)
|
||||
for (root, dirs, files) in os.walk(path)
|
||||
for name in files)
|
||||
for file_path in files:
|
||||
migrated = try_migrate(file_path)
|
||||
status = 'OK' if migrated else 'FAIL'
|
||||
sys.stderr.write("[{0}] {1}\n".format(status, file_path))
|
||||
sys.stderr.write("Done.\n")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
92
vcr/patch.py
92
vcr/patch.py
@@ -1,7 +1,7 @@
|
||||
'''Utilities for patching in cassettes'''
|
||||
|
||||
import httplib
|
||||
from .stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||
from six.moves import http_client as httplib
|
||||
|
||||
|
||||
# Save some of the original types for the purposes of unpatching
|
||||
@@ -12,7 +12,8 @@ try:
|
||||
# Try to save the original types for requests
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
_VerifiedHTTPSConnection = cpool.VerifiedHTTPSConnection
|
||||
_HTTPConnection = cpool.HTTPConnection
|
||||
_cpoolHTTPConnection = cpool.HTTPConnection
|
||||
_cpoolHTTPSConnection = cpool.HTTPSConnection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
@@ -23,16 +24,36 @@ try:
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
try:
|
||||
# Try to save the original types for httplib2
|
||||
import httplib2
|
||||
_HTTPConnectionWithTimeout = httplib2.HTTPConnectionWithTimeout
|
||||
_HTTPSConnectionWithTimeout = httplib2.HTTPSConnectionWithTimeout
|
||||
_SCHEME_TO_CONNECTION = httplib2.SCHEME_TO_CONNECTION
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
try:
|
||||
# Try to save the original types for boto
|
||||
import boto.https_connection
|
||||
_CertValidatingHTTPSConnection = \
|
||||
boto.https_connection.CertValidatingHTTPSConnection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
def install(cassette):
|
||||
'''Install a cassette in lieu of actuall fetching'''
|
||||
httplib.HTTPConnection = httplib.HTTP._connection_class = VCRHTTPConnection
|
||||
httplib.HTTPSConnection = httplib.HTTPS._connection_class = (
|
||||
VCRHTTPSConnection)
|
||||
"""
|
||||
Patch all the HTTPConnections references we can find!
|
||||
This replaces the actual HTTPConnection with a VCRHTTPConnection
|
||||
object which knows how to save to / read from cassettes
|
||||
"""
|
||||
httplib.HTTPConnection = VCRHTTPConnection
|
||||
httplib.HTTPSConnection = VCRHTTPSConnection
|
||||
httplib.HTTPConnection.cassette = cassette
|
||||
httplib.HTTPSConnection.cassette = cassette
|
||||
|
||||
# patch requests
|
||||
# patch requests v1.x
|
||||
try:
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
from .stubs.requests_stubs import VCRVerifiedHTTPSConnection
|
||||
@@ -40,6 +61,11 @@ def install(cassette):
|
||||
cpool.VerifiedHTTPSConnection.cassette = cassette
|
||||
cpool.HTTPConnection = VCRHTTPConnection
|
||||
cpool.HTTPConnection.cassette = cassette
|
||||
# patch requests v2.x
|
||||
cpool.HTTPConnectionPool.ConnectionCls = VCRHTTPConnection
|
||||
cpool.HTTPConnectionPool.cassette = cassette
|
||||
cpool.HTTPSConnectionPool.ConnectionCls = VCRHTTPSConnection
|
||||
cpool.HTTPSConnectionPool.cassette = cassette
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
@@ -54,16 +80,43 @@ def install(cassette):
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
# patch httplib2
|
||||
try:
|
||||
import httplib2 as cpool
|
||||
from .stubs.httplib2_stubs import VCRHTTPConnectionWithTimeout
|
||||
from .stubs.httplib2_stubs import VCRHTTPSConnectionWithTimeout
|
||||
cpool.HTTPConnectionWithTimeout = VCRHTTPConnectionWithTimeout
|
||||
cpool.HTTPSConnectionWithTimeout = VCRHTTPSConnectionWithTimeout
|
||||
cpool.SCHEME_TO_CONNECTION = {
|
||||
'http': VCRHTTPConnectionWithTimeout,
|
||||
'https': VCRHTTPSConnectionWithTimeout
|
||||
}
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
# patch boto
|
||||
try:
|
||||
import boto.https_connection as cpool
|
||||
from .stubs.boto_stubs import VCRCertValidatingHTTPSConnection
|
||||
cpool.CertValidatingHTTPSConnection = VCRCertValidatingHTTPSConnection
|
||||
cpool.CertValidatingHTTPSConnection.cassette = cassette
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
def reset():
|
||||
'''Undo all the patching'''
|
||||
httplib.HTTPConnection = httplib.HTTP._connection_class = _HTTPConnection
|
||||
httplib.HTTPSConnection = httplib.HTTPS._connection_class = \
|
||||
_HTTPSConnection
|
||||
httplib.HTTPConnection = _HTTPConnection
|
||||
httplib.HTTPSConnection = _HTTPSConnection
|
||||
try:
|
||||
import requests.packages.urllib3.connectionpool as cpool
|
||||
# unpatch requests v1.x
|
||||
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
|
||||
cpool.HTTPConnection = _HTTPConnection
|
||||
cpool.HTTPConnection = _cpoolHTTPConnection
|
||||
# unpatch requests v2.x
|
||||
cpool.HTTPConnectionPool.ConnectionCls = _cpoolHTTPConnection
|
||||
cpool.HTTPSConnection = _cpoolHTTPSConnection
|
||||
cpool.HTTPSConnectionPool.ConnectionCls = _cpoolHTTPSConnection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
@@ -71,5 +124,22 @@ def reset():
|
||||
import urllib3.connectionpool as cpool
|
||||
cpool.VerifiedHTTPSConnection = _VerifiedHTTPSConnection
|
||||
cpool.HTTPConnection = _HTTPConnection
|
||||
cpool.HTTPSConnection = _HTTPSConnection
|
||||
cpool.HTTPConnectionPool.ConnectionCls = _HTTPConnection
|
||||
cpool.HTTPSConnectionPool.ConnectionCls = _HTTPSConnection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
try:
|
||||
import httplib2 as cpool
|
||||
cpool.HTTPConnectionWithTimeout = _HTTPConnectionWithTimeout
|
||||
cpool.HTTPSConnectionWithTimeout = _HTTPSConnectionWithTimeout
|
||||
cpool.SCHEME_TO_CONNECTION = _SCHEME_TO_CONNECTION
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
try:
|
||||
import boto.https_connection as cpool
|
||||
cpool.CertValidatingHTTPSConnection = _CertValidatingHTTPSConnection
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from .persisters.filesystem import FilesystemPersister
|
||||
from .serialize import serialize, deserialize
|
||||
|
||||
|
||||
def load_cassette(cassette_path, serializer):
|
||||
with open(cassette_path) as f:
|
||||
return serializer.deserialize(f.read())
|
||||
cassette_content = f.read()
|
||||
cassette = deserialize(cassette_content, serializer)
|
||||
return cassette
|
||||
|
||||
|
||||
def save_cassette(cassette_path, cassette_dict, serializer):
|
||||
data = serializer.serialize(cassette_dict)
|
||||
data = serialize(cassette_dict, serializer)
|
||||
FilesystemPersister.write(cassette_path, data)
|
||||
|
||||
@@ -3,21 +3,10 @@ import os
|
||||
|
||||
|
||||
class FilesystemPersister(object):
|
||||
@classmethod
|
||||
def _secure_write(cls, path, contents):
|
||||
"""
|
||||
We'll overwrite the old version securely by writing out a temporary
|
||||
version and then moving it to replace the old version
|
||||
"""
|
||||
dirname, filename = os.path.split(path)
|
||||
fd, name = tempfile.mkstemp(dir=dirname, prefix=filename)
|
||||
with os.fdopen(fd, 'w') as fout:
|
||||
fout.write(contents)
|
||||
os.rename(name, path)
|
||||
|
||||
@classmethod
|
||||
def write(cls, cassette_path, data):
|
||||
dirname, filename = os.path.split(cassette_path)
|
||||
if dirname and not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
cls._secure_write(cassette_path, data)
|
||||
with open(cassette_path, 'w') as f:
|
||||
f.write(data)
|
||||
|
||||
100
vcr/request.py
100
vcr/request.py
@@ -1,50 +1,90 @@
|
||||
class Request(object):
|
||||
from six.moves.urllib.parse import urlparse, parse_qsl
|
||||
|
||||
def __init__(self, protocol, host, port, method, path, body, headers):
|
||||
self.protocol = protocol
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
class Request(object):
|
||||
"""
|
||||
VCR's representation of a request.
|
||||
|
||||
There is a weird quirk in HTTP. You can send the same header twice. For
|
||||
this reason, headers are represented by a dict, with lists as the values.
|
||||
However, it appears that HTTPlib is completely incapable of sending the
|
||||
same header twice. This puts me in a weird position: I want to be able to
|
||||
accurately represent HTTP headers in cassettes, but I don't want the extra
|
||||
step of always having to do [0] in the general case, i.e.
|
||||
request.headers['key'][0]
|
||||
|
||||
In addition, some servers sometimes send the same header more than once,
|
||||
and httplib *can* deal with this situation.
|
||||
|
||||
Futhermore, I wanted to keep the request and response cassette format as
|
||||
similar as possible.
|
||||
|
||||
For this reason, in cassettes I keep a dict with lists as keys, but once
|
||||
deserialized into VCR, I keep them as plain, naked dicts.
|
||||
"""
|
||||
|
||||
def __init__(self, method, uri, body, headers):
|
||||
self.method = method
|
||||
self.path = path
|
||||
self.uri = uri
|
||||
self.body = body
|
||||
# make headers a frozenset so it will be hashable
|
||||
self.headers = frozenset(headers.items())
|
||||
self.headers = {}
|
||||
for key in headers:
|
||||
self.add_header(key, headers[key])
|
||||
|
||||
def add_header(self, key, value):
|
||||
# see class docstring for an explanation
|
||||
if isinstance(value, (tuple, list)):
|
||||
self.headers[key] = value[0]
|
||||
else:
|
||||
self.headers[key] = value
|
||||
|
||||
@property
|
||||
def scheme(self):
|
||||
return urlparse(self.uri).scheme
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return urlparse(self.uri).hostname
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
parse_uri = urlparse(self.uri)
|
||||
port = parse_uri.port
|
||||
if port is None:
|
||||
port = {'https': 433, 'http': 80}[parse_uri.scheme]
|
||||
return port
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return urlparse(self.uri).path
|
||||
|
||||
@property
|
||||
def query(self):
|
||||
q = urlparse(self.uri).query
|
||||
return sorted(parse_qsl(q))
|
||||
|
||||
# alias for backwards compatibility
|
||||
@property
|
||||
def url(self):
|
||||
return "{0}://{1}{2}".format(self.protocol, self.host, self.path)
|
||||
return self.uri
|
||||
|
||||
def __key(self):
|
||||
return (
|
||||
self.host,
|
||||
self.port,
|
||||
self.method,
|
||||
self.path,
|
||||
self.body,
|
||||
self.headers
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__key())
|
||||
|
||||
def __eq__(self, other):
|
||||
return hash(self) == hash(other)
|
||||
# alias for backwards compatibility
|
||||
@property
|
||||
def protocol(self):
|
||||
return self.scheme
|
||||
|
||||
def __str__(self):
|
||||
return "<Request ({0}) {1}>".format(self.method, self.url)
|
||||
return "<Request ({0}) {1}>".format(self.method, self.uri)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def _to_dict(self):
|
||||
return {
|
||||
'protocol': self.protocol,
|
||||
'host': self.host,
|
||||
'port': self.port,
|
||||
'method': self.method,
|
||||
'path': self.path,
|
||||
'uri': self.uri,
|
||||
'body': self.body,
|
||||
'headers': self.headers,
|
||||
'headers': dict(((k, [v]) for k, v in self.headers.items())),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
||||
63
vcr/serialize.py
Normal file
63
vcr/serialize.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from vcr.serializers import compat
|
||||
from vcr.request import Request
|
||||
import yaml
|
||||
|
||||
# version 1 cassettes started with VCR 1.0.x.
|
||||
# Before 1.0.x, there was no versioning.
|
||||
CASSETTE_FORMAT_VERSION = 1
|
||||
|
||||
"""
|
||||
Just a general note on the serialization philosophy here:
|
||||
I prefer cassettes to be human-readable if possible. Yaml serializes
|
||||
bytestrings to !!binary, which isn't readable, so I would like to serialize to
|
||||
strings and from strings, which yaml will encode as utf-8 automatically.
|
||||
All the internal HTTP stuff expects bytestrings, so this whole serialization
|
||||
process feels backwards.
|
||||
|
||||
Serializing: bytestring -> string (yaml persists to utf-8)
|
||||
Deserializing: string (yaml converts from utf-8) -> bytestring
|
||||
"""
|
||||
|
||||
|
||||
def _looks_like_an_old_cassette(data):
|
||||
return isinstance(data, list) and len(data) and 'request' in data[0]
|
||||
|
||||
|
||||
def _warn_about_old_cassette_format():
|
||||
raise ValueError(
|
||||
"Your cassette files were generated in an older version "
|
||||
"of VCR. Delete your cassettes or run the migration script."
|
||||
"See http://git.io/mHhLBg for more details."
|
||||
)
|
||||
|
||||
|
||||
def deserialize(cassette_string, serializer):
|
||||
try:
|
||||
data = serializer.deserialize(cassette_string)
|
||||
# Old cassettes used to use yaml object thingy so I have to
|
||||
# check for some fairly stupid exceptions here
|
||||
except (ImportError, yaml.constructor.ConstructorError):
|
||||
_warn_about_old_cassette_format()
|
||||
if _looks_like_an_old_cassette(data):
|
||||
_warn_about_old_cassette_format()
|
||||
|
||||
requests = [Request._from_dict(r['request']) for r in data['interactions']]
|
||||
responses = [
|
||||
compat.convert_to_bytes(r['response']) for r in data['interactions']
|
||||
]
|
||||
return requests, responses
|
||||
|
||||
|
||||
def serialize(cassette_dict, serializer):
|
||||
interactions = ([{
|
||||
'request': request._to_dict(),
|
||||
'response': compat.convert_to_unicode(response),
|
||||
} for request, response in zip(
|
||||
cassette_dict['requests'],
|
||||
cassette_dict['responses'],
|
||||
)])
|
||||
data = {
|
||||
'version': CASSETTE_FORMAT_VERSION,
|
||||
'interactions': interactions,
|
||||
}
|
||||
return serializer.serialize(data)
|
||||
55
vcr/serializers/compat.py
Normal file
55
vcr/serializers/compat.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import six
|
||||
|
||||
|
||||
def convert_to_bytes(resp):
|
||||
resp = convert_body_to_bytes(resp)
|
||||
return resp
|
||||
|
||||
|
||||
def convert_to_unicode(resp):
|
||||
resp = convert_body_to_unicode(resp)
|
||||
return resp
|
||||
|
||||
|
||||
def convert_body_to_bytes(resp):
|
||||
"""
|
||||
If the request body is a string, encode it to bytes (for python3 support)
|
||||
|
||||
By default yaml serializes to utf-8 encoded bytestrings.
|
||||
When this cassette is loaded by python3, it's automatically decoded
|
||||
into unicode strings. This makes sure that it stays a bytestring, since
|
||||
that's what all the internal httplib machinery is expecting.
|
||||
|
||||
For more info on py3 yaml:
|
||||
http://pyyaml.org/wiki/PyYAMLDocumentation#Python3support
|
||||
"""
|
||||
try:
|
||||
if not isinstance(resp['body']['string'], six.binary_type):
|
||||
resp['body']['string'] = resp['body']['string'].encode('utf-8')
|
||||
except (KeyError, TypeError, UnicodeEncodeError):
|
||||
# The thing we were converting either wasn't a dictionary or didn't
|
||||
# have the keys we were expecting. Some of the tests just serialize
|
||||
# and deserialize a string.
|
||||
|
||||
# Also, sometimes the thing actually is binary, so if you can't encode
|
||||
# it, just give up.
|
||||
pass
|
||||
return resp
|
||||
|
||||
|
||||
def convert_body_to_unicode(resp):
|
||||
"""
|
||||
If the request body is bytes, decode it to a string (for python3 support)
|
||||
"""
|
||||
try:
|
||||
if not isinstance(resp['body']['string'], six.text_type):
|
||||
resp['body']['string'] = resp['body']['string'].decode('utf-8')
|
||||
except (KeyError, TypeError, UnicodeDecodeError):
|
||||
# The thing we were converting either wasn't a dictionary or didn't
|
||||
# have the keys we were expecting. Some of the tests just serialize
|
||||
# and deserialize a string.
|
||||
|
||||
# Also, sometimes the thing actually is binary, so if you can't decode
|
||||
# it, just give up.
|
||||
pass
|
||||
return resp
|
||||
@@ -1,34 +1,20 @@
|
||||
from vcr.request import Request
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
|
||||
def _json_default(obj):
|
||||
if isinstance(obj, frozenset):
|
||||
return dict(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def _fix_response_unicode(d):
|
||||
d['body']['string'] = d['body']['string'].encode('utf-8')
|
||||
return d
|
||||
|
||||
|
||||
def deserialize(cassette_string):
|
||||
data = json.loads(cassette_string)
|
||||
requests = [Request._from_dict(r['request']) for r in data]
|
||||
responses = [_fix_response_unicode(r['response']) for r in data]
|
||||
return requests, responses
|
||||
return json.loads(cassette_string)
|
||||
|
||||
|
||||
def serialize(cassette_dict):
|
||||
data = ([{
|
||||
'request': request._to_dict(),
|
||||
'response': response,
|
||||
} for request, response in zip(
|
||||
cassette_dict['requests'],
|
||||
cassette_dict['responses']
|
||||
)])
|
||||
return json.dumps(data, indent=4, default=_json_default)
|
||||
try:
|
||||
return json.dumps(cassette_dict, indent=4)
|
||||
except UnicodeDecodeError:
|
||||
raise UnicodeDecodeError(
|
||||
"Error serializing cassette to JSON. ",
|
||||
"Does this HTTP interaction contain binary data? ",
|
||||
"If so, use a different serializer (like the yaml serializer) ",
|
||||
"for this request"
|
||||
)
|
||||
|
||||
@@ -8,18 +8,8 @@ except ImportError:
|
||||
|
||||
|
||||
def deserialize(cassette_string):
|
||||
data = yaml.load(cassette_string, Loader=Loader)
|
||||
requests = [r['request'] for r in data]
|
||||
responses = [r['response'] for r in data]
|
||||
return requests, responses
|
||||
return yaml.load(cassette_string, Loader=Loader)
|
||||
|
||||
|
||||
def serialize(cassette_dict):
|
||||
data = ([{
|
||||
'request': request,
|
||||
'response': response,
|
||||
} for request, response in zip(
|
||||
cassette_dict['requests'],
|
||||
cassette_dict['responses']
|
||||
)])
|
||||
return yaml.dump(data, Dumper=Dumper)
|
||||
return yaml.dump(cassette_dict, Dumper=Dumper)
|
||||
|
||||
@@ -1,123 +1,309 @@
|
||||
'''Stubs for patching HTTP and HTTPS requests'''
|
||||
|
||||
from httplib import HTTPConnection, HTTPSConnection, HTTPMessage
|
||||
from cStringIO import StringIO
|
||||
|
||||
try:
|
||||
import http.client
|
||||
except ImportError:
|
||||
pass
|
||||
import logging
|
||||
import six
|
||||
from six.moves.http_client import (
|
||||
HTTPConnection,
|
||||
HTTPSConnection,
|
||||
HTTPMessage,
|
||||
HTTPResponse,
|
||||
)
|
||||
from six import BytesIO
|
||||
from vcr.request import Request
|
||||
from vcr.errors import CannotOverwriteExistingCassetteException
|
||||
from . import compat
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VCRHTTPResponse(object):
|
||||
class VCRFakeSocket(object):
|
||||
"""
|
||||
A socket that doesn't do anything!
|
||||
Used when playing back casssettes, when there
|
||||
is no actual open socket.
|
||||
"""
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def settimeout(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def fileno(self):
|
||||
"""
|
||||
This is kinda crappy. requests will watch
|
||||
this descriptor and make sure it's not closed.
|
||||
Return file descriptor 0 since that's stdin.
|
||||
"""
|
||||
return 0 # wonder how bad this is....
|
||||
|
||||
|
||||
def parse_headers(header_list):
|
||||
"""
|
||||
Convert headers from our serialized dict with lists for keys to a
|
||||
HTTPMessage
|
||||
"""
|
||||
header_string = b""
|
||||
for key, values in header_list.items():
|
||||
for v in values:
|
||||
header_string += \
|
||||
key.encode('utf-8') + b":" + v.encode('utf-8') + b"\r\n"
|
||||
return compat.get_httpmessage(header_string)
|
||||
|
||||
|
||||
def serialize_headers(response):
|
||||
out = {}
|
||||
for key, values in compat.get_headers(response):
|
||||
out.setdefault(key, [])
|
||||
out[key].extend(values)
|
||||
return out
|
||||
|
||||
|
||||
class VCRHTTPResponse(HTTPResponse):
|
||||
"""
|
||||
Stub reponse class that gets returned instead of a HTTPResponse
|
||||
"""
|
||||
def __init__(self, recorded_response):
|
||||
self.recorded_response = recorded_response
|
||||
self.reason = recorded_response['status']['message']
|
||||
self.status = recorded_response['status']['code']
|
||||
self.status = self.code = recorded_response['status']['code']
|
||||
self.version = None
|
||||
self._content = StringIO(self.recorded_response['body']['string'])
|
||||
self._content = BytesIO(self.recorded_response['body']['string'])
|
||||
self._closed = False
|
||||
|
||||
# We are skipping the header parsing (they have already been parsed
|
||||
# at this point) and directly adding the headers to the header
|
||||
# container, so just pass an empty StringIO.
|
||||
self.msg = HTTPMessage(StringIO(''))
|
||||
headers = self.recorded_response['headers']
|
||||
self.msg = parse_headers(headers)
|
||||
|
||||
for key, val in self.recorded_response['headers'].iteritems():
|
||||
self.msg.addheader(key, val)
|
||||
# msg.addheaders adds the headers to msg.dict, but not to
|
||||
# the msg.headers list representation of headers, so
|
||||
# I have to add it to both.
|
||||
self.msg.headers.append("{0}:{1}".format(key, val))
|
||||
self.length = compat.get_header(self.msg, 'content-length') or None
|
||||
|
||||
self.length = self.msg.getheader('content-length') or None
|
||||
@property
|
||||
def closed(self):
|
||||
# in python3, I can't change the value of self.closed. So I'
|
||||
# twiddling self._closed and using this property to shadow the real
|
||||
# self.closed from the superclas
|
||||
return self._closed
|
||||
|
||||
def read(self, *args, **kwargs):
|
||||
# Note: I'm pretty much ignoring any chunking stuff because
|
||||
# I don't really understand what it is or how it works.
|
||||
return self._content.read(*args, **kwargs)
|
||||
|
||||
def readline(self, *args, **kwargs):
|
||||
return self._content.readline(*args, **kwargs)
|
||||
|
||||
def close(self):
|
||||
self._closed = True
|
||||
return True
|
||||
|
||||
def getcode(self):
|
||||
return self.status
|
||||
|
||||
def isclosed(self):
|
||||
# Urllib3 seems to call this because it actually uses
|
||||
# the weird chunking support in httplib
|
||||
return True
|
||||
return self.closed
|
||||
|
||||
def info(self):
|
||||
return parse_headers(self.recorded_response['headers'])
|
||||
|
||||
def getheaders(self):
|
||||
return self.recorded_response['headers'].iteritems()
|
||||
message = parse_headers(self.recorded_response['headers'])
|
||||
return compat.get_header_items(message)
|
||||
|
||||
def getheader(self, header, default=None):
|
||||
headers = dict(((k, v) for k, v in self.getheaders()))
|
||||
return headers.get(header, default)
|
||||
|
||||
|
||||
class VCRConnectionMixin:
|
||||
class VCRConnection:
|
||||
# A reference to the cassette that's currently being patched in
|
||||
cassette = None
|
||||
|
||||
def _port_postfix(self):
|
||||
"""
|
||||
Returns empty string for the default port and ':port' otherwise
|
||||
"""
|
||||
port = self.real_connection.port
|
||||
default_port = {'https': 433, 'http': 80}[self._protocol]
|
||||
return ':{0}'.format(port) if port != default_port else ''
|
||||
|
||||
def _uri(self, url):
|
||||
"""Returns request absolute URI"""
|
||||
uri = "{0}://{1}{2}{3}".format(
|
||||
self._protocol,
|
||||
self.real_connection.host,
|
||||
self._port_postfix(),
|
||||
url,
|
||||
)
|
||||
return uri
|
||||
|
||||
def _url(self, uri):
|
||||
"""Returns request selector url from absolute URI"""
|
||||
prefix = "{0}://{1}{2}".format(
|
||||
self._protocol,
|
||||
self.real_connection.host,
|
||||
self._port_postfix(),
|
||||
)
|
||||
return uri.replace(prefix, '', 1)
|
||||
|
||||
def request(self, method, url, body=None, headers=None):
|
||||
'''Persist the request metadata in self._vcr_request'''
|
||||
self._vcr_request = Request(
|
||||
protocol=self._protocol,
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
method=method,
|
||||
path=url,
|
||||
uri=self._uri(url),
|
||||
body=body,
|
||||
headers=headers or {}
|
||||
)
|
||||
log.debug('Got {0}'.format(self._vcr_request))
|
||||
|
||||
# Check if we have a cassette set, and if we have a response saved.
|
||||
# If so, there's no need to keep processing and we can bail
|
||||
if self.cassette and self._vcr_request in self.cassette:
|
||||
return
|
||||
# Note: The request may not actually be finished at this point, so
|
||||
# I'm not sending the actual request until getresponse(). This
|
||||
# allows me to compare the entire length of the response to see if it
|
||||
# exists in the cassette.
|
||||
|
||||
# Otherwise, we should submit the request
|
||||
self._baseclass.request(
|
||||
self, method, url, body=body, headers=headers or {})
|
||||
def putrequest(self, method, url, *args, **kwargs):
|
||||
"""
|
||||
httplib gives you more than one way to do it. This is a way
|
||||
to start building up a request. Usually followed by a bunch
|
||||
of putheader() calls.
|
||||
"""
|
||||
self._vcr_request = Request(
|
||||
method=method,
|
||||
uri=self._uri(url),
|
||||
body="",
|
||||
headers={}
|
||||
)
|
||||
log.debug('Got {0}'.format(self._vcr_request))
|
||||
|
||||
def putheader(self, header, *values):
|
||||
for value in values:
|
||||
self._vcr_request.add_header(header, value)
|
||||
|
||||
def send(self, data):
|
||||
'''
|
||||
This method is called after request(), to add additional data to the
|
||||
body of the request. So if that happens, let's just append the data
|
||||
onto the most recent request in the cassette.
|
||||
'''
|
||||
self._vcr_request.body = (self._vcr_request.body or '') + data
|
||||
|
||||
def close(self):
|
||||
# Note: the real connection will only close if it's open, so
|
||||
# no need to check that here.
|
||||
self.real_connection.close()
|
||||
|
||||
def endheaders(self, *args, **kwargs):
|
||||
"""
|
||||
Normally, this would atually send the request to the server.
|
||||
We are not sending the request until getting the response,
|
||||
so bypass this method for now.
|
||||
"""
|
||||
pass
|
||||
|
||||
def getresponse(self, _=False):
|
||||
'''Retrieve a the response'''
|
||||
# Check to see if the cassette has a response for this request. If so,
|
||||
# then return it
|
||||
if self._vcr_request in self.cassette:
|
||||
response = self.cassette.response_of(self._vcr_request)
|
||||
# Alert the cassette to the fact that we've served another
|
||||
# response for the provided requests
|
||||
self.cassette.mark_played(self._vcr_request)
|
||||
if self.cassette.can_play_response_for(self._vcr_request):
|
||||
log.info(
|
||||
"Playing response for {0} from cassette".format(
|
||||
self._vcr_request
|
||||
)
|
||||
)
|
||||
response = self.cassette.play_response(self._vcr_request)
|
||||
return VCRHTTPResponse(response)
|
||||
else:
|
||||
# Otherwise, we made an actual request, and should return the
|
||||
# response we got from the actual connection
|
||||
response = HTTPConnection.getresponse(self)
|
||||
if self.cassette.write_protected and self.cassette._filter_request(self._vcr_request):
|
||||
raise CannotOverwriteExistingCassetteException(
|
||||
"Can't overwrite existing cassette (%r) in "
|
||||
"your current record mode (%r)."
|
||||
% (self.cassette._path, self.cassette.record_mode)
|
||||
)
|
||||
|
||||
# Otherwise, we should send the request, then get the response
|
||||
# and return it.
|
||||
|
||||
log.info(
|
||||
"{0} not in cassette, sending to real server".format(
|
||||
self._vcr_request
|
||||
)
|
||||
)
|
||||
self.real_connection.request(
|
||||
method=self._vcr_request.method,
|
||||
url=self._url(self._vcr_request.uri),
|
||||
body=self._vcr_request.body,
|
||||
headers=self._vcr_request.headers,
|
||||
)
|
||||
|
||||
# get the response
|
||||
response = self.real_connection.getresponse()
|
||||
|
||||
# put the response into the cassette
|
||||
response = {
|
||||
'status': {
|
||||
'code': response.status,
|
||||
'message': response.reason
|
||||
},
|
||||
'headers': dict(response.getheaders()),
|
||||
'headers': serialize_headers(response),
|
||||
'body': {'string': response.read()},
|
||||
}
|
||||
self.cassette.append(self._vcr_request, response)
|
||||
return VCRHTTPResponse(response)
|
||||
|
||||
def set_debuglevel(self, *args, **kwargs):
|
||||
self.real_connection.set_debuglevel(*args, **kwargs)
|
||||
|
||||
class VCRHTTPConnection(VCRConnectionMixin, HTTPConnection):
|
||||
def connect(self, *args, **kwargs):
|
||||
"""
|
||||
httplib2 uses this. Connects to the server I'm assuming.
|
||||
|
||||
Only pass to the baseclass if we don't have a recorded response
|
||||
and are not write-protected.
|
||||
"""
|
||||
|
||||
if hasattr(self, '_vcr_request') and \
|
||||
self.cassette.can_play_response_for(self._vcr_request):
|
||||
# We already have a response we are going to play, don't
|
||||
# actually connect
|
||||
return
|
||||
|
||||
if self.cassette.write_protected:
|
||||
# Cassette is write-protected, don't actually connect
|
||||
return
|
||||
|
||||
return self.real_connection.connect(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def sock(self):
|
||||
if self.real_connection.sock:
|
||||
return self.real_connection.sock
|
||||
return VCRFakeSocket()
|
||||
|
||||
@sock.setter
|
||||
def sock(self, value):
|
||||
if self.real_connection.sock:
|
||||
self.real_connection.sock = value
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if six.PY3:
|
||||
kwargs.pop('strict', None) # apparently this is gone in py3
|
||||
|
||||
# need to temporarily reset here because the real connection
|
||||
# inherits from the thing that we are mocking out. Take out
|
||||
# the reset if you want to see what I mean :)
|
||||
from vcr.patch import install, reset
|
||||
reset()
|
||||
self.real_connection = self._baseclass(*args, **kwargs)
|
||||
install(self.cassette)
|
||||
|
||||
|
||||
class VCRHTTPConnection(VCRConnection):
|
||||
'''A Mocked class for HTTP requests'''
|
||||
# Can't use super since this is an old-style class
|
||||
_baseclass = HTTPConnection
|
||||
_protocol = 'http'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
HTTPConnection.__init__(self, *args, **kwargs)
|
||||
|
||||
|
||||
class VCRHTTPSConnection(VCRConnectionMixin, HTTPSConnection):
|
||||
class VCRHTTPSConnection(VCRConnection):
|
||||
'''A Mocked class for HTTPS requests'''
|
||||
_baseclass = HTTPSConnection
|
||||
_protocol = 'https'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
'''I overrode the init and copied a lot of the code from the parent
|
||||
class because HTTPConnection when this happens has been replaced by
|
||||
VCRHTTPConnection, but doing it here lets us use the original one.'''
|
||||
HTTPConnection.__init__(self, *args, **kwargs)
|
||||
self.key_file = kwargs.pop('key_file', None)
|
||||
self.cert_file = kwargs.pop('cert_file', None)
|
||||
|
||||
8
vcr/stubs/boto_stubs.py
Normal file
8
vcr/stubs/boto_stubs.py
Normal file
@@ -0,0 +1,8 @@
|
||||
'''Stubs for boto'''
|
||||
|
||||
from boto.https_connection import CertValidatingHTTPSConnection
|
||||
from ..stubs import VCRHTTPSConnection
|
||||
|
||||
|
||||
class VCRCertValidatingHTTPSConnection(VCRHTTPSConnection):
|
||||
_baseclass = CertValidatingHTTPSConnection
|
||||
44
vcr/stubs/compat.py
Normal file
44
vcr/stubs/compat.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import six
|
||||
from six import BytesIO
|
||||
from six.moves.http_client import HTTPMessage
|
||||
try:
|
||||
import http.client
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
"""
|
||||
The python3 http.client api moved some stuff around, so this is an abstraction
|
||||
layer that tries to cope with this move.
|
||||
"""
|
||||
|
||||
|
||||
def get_header(message, name):
|
||||
if six.PY3:
|
||||
return message.getallmatchingheaders(name)
|
||||
else:
|
||||
return message.getheader(name)
|
||||
|
||||
|
||||
def get_header_items(message):
|
||||
if six.PY3:
|
||||
return dict(message._headers).items()
|
||||
else:
|
||||
return message.dict.items()
|
||||
|
||||
|
||||
def get_headers(response):
|
||||
for key in response.msg.keys():
|
||||
if six.PY3:
|
||||
yield key, response.msg.get_all(key)
|
||||
else:
|
||||
yield key, response.msg.getheaders(key)
|
||||
|
||||
|
||||
def get_httpmessage(headers):
|
||||
if six.PY3:
|
||||
return http.client.parse_headers(BytesIO(headers))
|
||||
msg = HTTPMessage(BytesIO(headers))
|
||||
msg.fp.seek(0)
|
||||
msg.readheaders()
|
||||
return msg
|
||||
62
vcr/stubs/httplib2_stubs.py
Normal file
62
vcr/stubs/httplib2_stubs.py
Normal file
@@ -0,0 +1,62 @@
|
||||
'''Stubs for httplib2'''
|
||||
|
||||
from httplib2 import HTTPConnectionWithTimeout, HTTPSConnectionWithTimeout
|
||||
from ..stubs import VCRHTTPConnection, VCRHTTPSConnection
|
||||
|
||||
|
||||
class VCRHTTPConnectionWithTimeout(VCRHTTPConnection,
|
||||
HTTPConnectionWithTimeout):
|
||||
_baseclass = HTTPConnectionWithTimeout
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
'''I overrode the init because I need to clean kwargs before calling
|
||||
HTTPConnection.__init__.'''
|
||||
|
||||
# Delete the keyword arguments that HTTPConnection would not recognize
|
||||
safe_keys = set(
|
||||
('host', 'port', 'strict', 'timeout', 'source_address')
|
||||
)
|
||||
unknown_keys = set(kwargs.keys()) - safe_keys
|
||||
safe_kwargs = kwargs.copy()
|
||||
for kw in unknown_keys:
|
||||
del safe_kwargs[kw]
|
||||
|
||||
self.proxy_info = kwargs.pop('proxy_info', None)
|
||||
VCRHTTPConnection.__init__(self, *args, **safe_kwargs)
|
||||
self.sock = self.real_connection.sock
|
||||
|
||||
|
||||
class VCRHTTPSConnectionWithTimeout(VCRHTTPSConnection,
|
||||
HTTPSConnectionWithTimeout):
|
||||
_baseclass = HTTPSConnectionWithTimeout
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Delete the keyword arguments that HTTPSConnection would not recognize
|
||||
safe_keys = set((
|
||||
'host',
|
||||
'port',
|
||||
'key_file',
|
||||
'cert_file',
|
||||
'strict',
|
||||
'timeout',
|
||||
'source_address',
|
||||
))
|
||||
unknown_keys = set(kwargs.keys()) - safe_keys
|
||||
safe_kwargs = kwargs.copy()
|
||||
for kw in unknown_keys:
|
||||
del safe_kwargs[kw]
|
||||
self.proxy_info = kwargs.pop('proxy_info', None)
|
||||
if 'ca_certs' not in kwargs or kwargs['ca_certs'] is None:
|
||||
try:
|
||||
import httplib2
|
||||
self.ca_certs = httplib2.CA_CERTS
|
||||
except ImportError:
|
||||
self.ca_certs = None
|
||||
else:
|
||||
self.ca_certs = kwargs['ca_certs']
|
||||
|
||||
self.disable_ssl_certificate_validation = kwargs.pop(
|
||||
'disable_ssl_certificate_validation', None)
|
||||
VCRHTTPSConnection.__init__(self, *args, **safe_kwargs)
|
||||
self.sock = self.real_connection.sock
|
||||
Reference in New Issue
Block a user