"""Test requests' interaction with vcr""" import asyncio import functools import inspect import json import os import ssl import pytest import vcr from vcr.errors import CannotOverwriteExistingCassetteException from ..assertions import assert_cassette_empty, assert_is_json_bytes tornado = pytest.importorskip("tornado") gen = pytest.importorskip("tornado.gen") http = pytest.importorskip("tornado.httpclient") # whether the current version of Tornado supports the raise_error argument for # fetch(). supports_raise_error = tornado.version_info >= (4,) raise_error_for_response_code_only = tornado.version_info >= (6,) def gen_test(func): @functools.wraps(func) def wrapper(*args, **kwargs): async def coro(): return await gen.coroutine(func)(*args, **kwargs) return asyncio.run(coro()) # Patch the signature so pytest can inject fixtures # we can't use wrapt.decorator because it returns a generator function wrapper.__signature__ = inspect.signature(func) return wrapper @pytest.fixture(params=["simple", "curl", "default"]) def get_client(request): ca_bundle_path = os.environ.get("REQUESTS_CA_BUNDLE") ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) ssl_ctx.load_verify_locations(cafile=ca_bundle_path) ssl_ctx.verify_mode = ssl.CERT_REQUIRED if request.param == "simple": from tornado import simple_httpclient as simple return lambda: simple.SimpleAsyncHTTPClient(defaults={"ssl_options": ssl_ctx}) if request.param == "curl": curl = pytest.importorskip("tornado.curl_httpclient") return lambda: curl.CurlAsyncHTTPClient(defaults={"ca_certs": ca_bundle_path}) return lambda: http.AsyncHTTPClient(defaults={"ssl_options": ssl_ctx}) def get(client, url, **kwargs): fetch_kwargs = {} if supports_raise_error: fetch_kwargs["raise_error"] = kwargs.pop("raise_error", True) return client.fetch(http.HTTPRequest(url, method="GET", **kwargs), **fetch_kwargs) def post(client, url, data=None, **kwargs): if data: kwargs["body"] = json.dumps(data) return client.fetch(http.HTTPRequest(url, method="POST", **kwargs)) @pytest.mark.online @gen_test def test_status_code(get_client, tmpdir, httpbin_both): """Ensure that we can read the status code""" url = httpbin_both.url with vcr.use_cassette(str(tmpdir.join("atts.yaml"))): status_code = (yield get(get_client(), url)).code with vcr.use_cassette(str(tmpdir.join("atts.yaml"))) as cass: assert status_code == (yield get(get_client(), url)).code assert cass.play_count == 1 @pytest.mark.online @gen_test def test_headers(get_client, httpbin_both, tmpdir): """Ensure that we can read the headers back""" url = httpbin_both.url with vcr.use_cassette(str(tmpdir.join("headers.yaml"))): headers = (yield get(get_client(), url)).headers with vcr.use_cassette(str(tmpdir.join("headers.yaml"))) as cass: assert headers == (yield get(get_client(), url)).headers assert cass.play_count == 1 @pytest.mark.online @gen_test def test_body(get_client, tmpdir, httpbin_both): """Ensure the responses are all identical enough""" url = httpbin_both.url + "/bytes/1024" with vcr.use_cassette(str(tmpdir.join("body.yaml"))): content = (yield get(get_client(), url)).body with vcr.use_cassette(str(tmpdir.join("body.yaml"))) as cass: assert content == (yield get(get_client(), url)).body assert cass.play_count == 1 @gen_test def test_effective_url(get_client, tmpdir, httpbin): """Ensure that the effective_url is captured""" url = httpbin.url + "/redirect/1" with vcr.use_cassette(str(tmpdir.join("url.yaml"))): effective_url = (yield get(get_client(), url)).effective_url assert effective_url == httpbin.url + "/get" with vcr.use_cassette(str(tmpdir.join("url.yaml"))) as cass: assert effective_url == (yield get(get_client(), url)).effective_url assert cass.play_count == 1 @pytest.mark.online @gen_test def test_auth(get_client, tmpdir, httpbin_both): """Ensure that we can handle basic auth""" auth = ("user", "passwd") url = httpbin_both.url + "/basic-auth/user/passwd" with vcr.use_cassette(str(tmpdir.join("auth.yaml"))): one = yield get(get_client(), url, auth_username=auth[0], auth_password=auth[1]) with vcr.use_cassette(str(tmpdir.join("auth.yaml"))) as cass: two = yield get(get_client(), url, auth_username=auth[0], auth_password=auth[1]) assert one.body == two.body assert one.code == two.code assert cass.play_count == 1 @pytest.mark.online @gen_test def test_auth_failed(get_client, tmpdir, httpbin_both): """Ensure that we can save failed auth statuses""" auth = ("user", "wrongwrongwrong") url = httpbin_both.url + "/basic-auth/user/passwd" with vcr.use_cassette(str(tmpdir.join("auth-failed.yaml"))) as cass: # Ensure that this is empty to begin with assert_cassette_empty(cass) with pytest.raises(http.HTTPError) as exc_info: yield get(get_client(), url, auth_username=auth[0], auth_password=auth[1]) one = exc_info.value.response assert exc_info.value.code == 401 with vcr.use_cassette(str(tmpdir.join("auth-failed.yaml"))) as cass: with pytest.raises(http.HTTPError) as exc_info: two = yield get(get_client(), url, auth_username=auth[0], auth_password=auth[1]) two = exc_info.value.response assert exc_info.value.code == 401 assert one.body == two.body assert one.code == two.code == 401 assert cass.play_count == 1 @pytest.mark.online @gen_test def test_post(get_client, tmpdir, httpbin_both): """Ensure that we can post and cache the results""" data = {"key1": "value1", "key2": "value2"} url = httpbin_both.url + "/post" with vcr.use_cassette(str(tmpdir.join("requests.yaml"))): req1 = (yield post(get_client(), url, data)).body with vcr.use_cassette(str(tmpdir.join("requests.yaml"))) as cass: req2 = (yield post(get_client(), url, data)).body assert req1 == req2 assert cass.play_count == 1 @gen_test def test_redirects(get_client, tmpdir, httpbin): """Ensure that we can handle redirects""" url = httpbin + "/redirect-to?url=bytes/1024&status_code=301" with vcr.use_cassette(str(tmpdir.join("requests.yaml"))): content = (yield get(get_client(), url)).body with vcr.use_cassette(str(tmpdir.join("requests.yaml"))) as cass: assert content == (yield get(get_client(), url)).body assert cass.play_count == 1 @pytest.mark.online @gen_test def test_cross_scheme(get_client, tmpdir, httpbin, httpbin_secure): """Ensure that requests between schemes are treated separately""" # First fetch a url under http, 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 url = httpbin.url url_secure = httpbin_secure.url with vcr.use_cassette(str(tmpdir.join("cross_scheme.yaml"))) as cass: yield get(get_client(), url) yield get(get_client(), url_secure) assert cass.play_count == 0 assert len(cass) == 2 # Then repeat the same requests and ensure both were replayed. with vcr.use_cassette(str(tmpdir.join("cross_scheme.yaml"))) as cass: yield get(get_client(), url) yield get(get_client(), url_secure) assert cass.play_count == 2 @pytest.mark.online @gen_test def test_gzip(get_client, tmpdir, httpbin_both): """ Ensure that httpclient is able to automatically decompress the response body """ url = httpbin_both + "/gzip" # use_gzip was renamed to decompress_response in 4.0 kwargs = {} if tornado.version_info < (4,): kwargs["use_gzip"] = True else: kwargs["decompress_response"] = True with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))): response = yield get(get_client(), url, **kwargs) assert_is_json_bytes(response.body) with vcr.use_cassette(str(tmpdir.join("gzip.yaml"))) as cass: response = yield get(get_client(), url, **kwargs) assert_is_json_bytes(response.body) assert cass.play_count == 1 @pytest.mark.online @gen_test def test_https_with_cert_validation_disabled(get_client, tmpdir, httpbin_secure): cass_path = str(tmpdir.join("cert_validation_disabled.yaml")) url = httpbin_secure.url with vcr.use_cassette(cass_path): yield get(get_client(), url, validate_cert=False) with vcr.use_cassette(cass_path) as cass: yield get(get_client(), url, validate_cert=False) assert cass.play_count == 1 @gen_test def test_unsupported_features_raises_in_future(get_client, tmpdir, httpbin): """Ensure that the exception for an AsyncHTTPClient feature not being supported is raised inside the future.""" def callback(chunk): raise AssertionError("Did not expect to be called.") with vcr.use_cassette(str(tmpdir.join("invalid.yaml"))): future = get(get_client(), httpbin.url, streaming_callback=callback) with pytest.raises(Exception) as excinfo: yield future assert "not yet supported by VCR" in str(excinfo) @pytest.mark.skipif(not supports_raise_error, reason="raise_error unavailable in tornado <= 3") @pytest.mark.skipif( raise_error_for_response_code_only, reason="raise_error only ignores HTTPErrors due to response code", ) @gen_test def test_unsupported_features_raise_error_disabled(get_client, tmpdir): """Ensure that the exception for an AsyncHTTPClient feature not being supported is not raised if raise_error=False.""" def callback(chunk): raise AssertionError("Did not expect to be called.") with vcr.use_cassette(str(tmpdir.join("invalid.yaml"))): response = yield get( get_client(), "http://httpbin.org", streaming_callback=callback, raise_error=False, ) assert "not yet supported by VCR" in str(response.error) @pytest.mark.online @gen_test def test_cannot_overwrite_cassette_raises_in_future(get_client, tmpdir, httpbin): """Ensure that CannotOverwriteExistingCassetteException is raised inside the future.""" url = httpbin.url with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))): yield get(get_client(), url + "/get") with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))): future = get(get_client(), url + "/headers") with pytest.raises(CannotOverwriteExistingCassetteException): yield future @pytest.mark.skipif(not supports_raise_error, reason="raise_error unavailable in tornado <= 3") @pytest.mark.skipif( raise_error_for_response_code_only, reason="raise_error only ignores HTTPErrors due to response code", ) @gen_test def test_cannot_overwrite_cassette_raise_error_disabled(get_client, tmpdir, httpbin): """Ensure that CannotOverwriteExistingCassetteException is not raised if raise_error=False in the fetch() call.""" url = httpbin.url with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))): yield get(get_client(), url + "/get", raise_error=False) with vcr.use_cassette(str(tmpdir.join("overwrite.yaml"))): response = yield get(get_client(), url + "/headers", raise_error=False) assert isinstance(response.error, CannotOverwriteExistingCassetteException) @gen_test @vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix(".yaml")) def test_tornado_with_decorator_use_cassette(get_client): response = yield get_client().fetch(http.HTTPRequest("http://www.google.com/", method="GET")) assert response.body.decode("utf-8") == "not actually google" @gen_test @vcr.use_cassette(path_transformer=vcr.default_vcr.ensure_suffix(".yaml")) def test_tornado_exception_can_be_caught(get_client): try: yield get(get_client(), "http://httpbin.org/status/500") except http.HTTPError as e: assert e.code == 500 try: yield get(get_client(), "http://httpbin.org/status/404") except http.HTTPError as e: assert e.code == 404 @pytest.mark.online @gen_test def test_existing_references_get_patched(tmpdir, httpbin): from tornado.httpclient import AsyncHTTPClient url = httpbin.url + "/get" with vcr.use_cassette(str(tmpdir.join("data.yaml"))): client = AsyncHTTPClient() yield get(client, url) with vcr.use_cassette(str(tmpdir.join("data.yaml"))) as cass: yield get(client, url) assert cass.play_count == 1 @pytest.mark.online @gen_test def test_existing_instances_get_patched(get_client, tmpdir, httpbin): """Ensure that existing instances of AsyncHTTPClient get patched upon entering VCR context.""" url = httpbin.url + "/get" client = get_client() with vcr.use_cassette(str(tmpdir.join("data.yaml"))): yield get(client, url) with vcr.use_cassette(str(tmpdir.join("data.yaml"))) as cass: yield get(client, url) assert cass.play_count == 1 @pytest.mark.online @gen_test def test_request_time_is_set(get_client, tmpdir, httpbin): """Ensures that the request_time on HTTPResponses is set.""" url = httpbin.url + "/get" with vcr.use_cassette(str(tmpdir.join("data.yaml"))): client = get_client() response = yield get(client, url) assert response.request_time is not None with vcr.use_cassette(str(tmpdir.join("data.yaml"))) as cass: client = get_client() response = yield get(client, url) assert response.request_time is not None assert cass.play_count == 1