Refactor tests to use MockTransport(<handler_function>) (#1281)

* Support Response(content=<bytes iterator>)

* Update test for merged master

* Add MockTransport for test cases

* Use MockTransport for redirect tests

* Reduce change footprint

* Reduce change footprint

* Clean up headers slightly

* Update requirements.txt

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
This commit is contained in:
Tom Christie 2020-09-12 11:16:10 +01:00 committed by GitHub
parent 8a5050ea41
commit c2afd2d9bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 281 additions and 331 deletions

View File

@ -18,7 +18,7 @@ black==20.8b1
cryptography
flake8
flake8-bugbear
flake8-pie
flake8-pie==0.5.*
isort==5.*
mypy
pytest==5.*

View File

@ -1,43 +1,19 @@
import typing
import json
from http.cookiejar import Cookie, CookieJar
import httpcore
import httpx
from httpx._content_streams import ByteStream, ContentStream, JSONStream
from tests.utils import MockTransport
def get_header_value(headers, key, default=None):
lookup = key.encode("ascii").lower()
for header_key, header_value in headers:
if header_key.lower() == lookup:
return header_value.decode("ascii")
return default
class MockTransport(httpcore.SyncHTTPTransport):
def request(
self,
method: bytes,
url: typing.Tuple[bytes, bytes, typing.Optional[int], bytes],
headers: typing.List[typing.Tuple[bytes, bytes]] = None,
stream: httpcore.SyncByteStream = None,
timeout: typing.Mapping[str, typing.Optional[float]] = None,
) -> typing.Tuple[
bytes, int, bytes, typing.List[typing.Tuple[bytes, bytes]], ContentStream
]:
host, scheme, port, path = url
body: ContentStream
if path.startswith(b"/echo_cookies"):
cookie = get_header_value(headers, "cookie")
body = JSONStream({"cookies": cookie})
return b"HTTP/1.1", 200, b"OK", [], body
elif path.startswith(b"/set_cookie"):
headers = [(b"set-cookie", b"example-name=example-value")]
body = ByteStream(b"")
return b"HTTP/1.1", 200, b"OK", headers, body
else:
raise NotImplementedError() # pragma: no cover
def get_and_set_cookies(request: httpx.Request) -> httpx.Response:
if request.url.path == "/echo_cookies":
data = {"cookies": request.headers.get("cookie")}
content = json.dumps(data).encode("utf-8")
return httpx.Response(200, content=content)
elif request.url.path == "/set_cookie":
return httpx.Response(200, headers={"set-cookie": "example-name=example-value"})
else:
raise NotImplementedError() # pragma: no cover
def test_set_cookie() -> None:
@ -47,7 +23,7 @@ def test_set_cookie() -> None:
url = "http://example.org/echo_cookies"
cookies = {"example-name": "example-value"}
client = httpx.Client(transport=MockTransport())
client = httpx.Client(transport=MockTransport(get_and_set_cookies))
response = client.get(url, cookies=cookies)
assert response.status_code == 200
@ -82,7 +58,7 @@ def test_set_cookie_with_cookiejar() -> None:
)
cookies.set_cookie(cookie)
client = httpx.Client(transport=MockTransport())
client = httpx.Client(transport=MockTransport(get_and_set_cookies))
response = client.get(url, cookies=cookies)
assert response.status_code == 200
@ -117,7 +93,7 @@ def test_setting_client_cookies_to_cookiejar() -> None:
)
cookies.set_cookie(cookie)
client = httpx.Client(transport=MockTransport())
client = httpx.Client(transport=MockTransport(get_and_set_cookies))
client.cookies = cookies # type: ignore
response = client.get(url)
@ -134,7 +110,7 @@ def test_set_cookie_with_cookies_model() -> None:
cookies = httpx.Cookies()
cookies["example-name"] = "example-value"
client = httpx.Client(transport=MockTransport())
client = httpx.Client(transport=MockTransport(get_and_set_cookies))
response = client.get(url, cookies=cookies)
assert response.status_code == 200
@ -144,7 +120,7 @@ def test_set_cookie_with_cookies_model() -> None:
def test_get_cookie() -> None:
url = "http://example.org/set_cookie"
client = httpx.Client(transport=MockTransport())
client = httpx.Client(transport=MockTransport(get_and_set_cookies))
response = client.get(url)
assert response.status_code == 200
@ -156,7 +132,7 @@ def test_cookie_persistence() -> None:
"""
Ensure that Client instances persist cookies between requests.
"""
client = httpx.Client(transport=MockTransport())
client = httpx.Client(transport=MockTransport(get_and_set_cookies))
response = client.get("http://example.org/echo_cookies")
assert response.status_code == 200

View File

@ -1,31 +1,17 @@
#!/usr/bin/env python3
import typing
import json
import httpcore
import pytest
import httpx
from httpx._content_streams import ContentStream, JSONStream
from tests.utils import MockTransport
class MockTransport(httpcore.SyncHTTPTransport):
def request(
self,
method: bytes,
url: typing.Tuple[bytes, bytes, typing.Optional[int], bytes],
headers: typing.List[typing.Tuple[bytes, bytes]] = None,
stream: httpcore.SyncByteStream = None,
timeout: typing.Mapping[str, typing.Optional[float]] = None,
) -> typing.Tuple[
bytes, int, bytes, typing.List[typing.Tuple[bytes, bytes]], ContentStream
]:
assert headers is not None
headers_dict = {
key.decode("ascii"): value.decode("ascii") for key, value in headers
}
body = JSONStream({"headers": headers_dict})
return b"HTTP/1.1", 200, b"OK", [], body
def echo_headers(request: httpx.Request) -> httpx.Response:
data = {"headers": dict(request.headers)}
content = json.dumps(data).encode("utf-8")
return httpx.Response(200, content=content)
def test_client_header():
@ -35,7 +21,7 @@ def test_client_header():
url = "http://example.org/echo_headers"
headers = {"Example-Header": "example-value"}
client = httpx.Client(transport=MockTransport(), headers=headers)
client = httpx.Client(transport=MockTransport(echo_headers), headers=headers)
response = client.get(url)
assert response.status_code == 200
@ -55,7 +41,7 @@ def test_header_merge():
url = "http://example.org/echo_headers"
client_headers = {"User-Agent": "python-myclient/0.2.1"}
request_headers = {"X-Auth-Token": "FooBarBazToken"}
client = httpx.Client(transport=MockTransport(), headers=client_headers)
client = httpx.Client(transport=MockTransport(echo_headers), headers=client_headers)
response = client.get(url, headers=request_headers)
assert response.status_code == 200
@ -75,7 +61,7 @@ def test_header_merge_conflicting_headers():
url = "http://example.org/echo_headers"
client_headers = {"X-Auth-Token": "FooBar"}
request_headers = {"X-Auth-Token": "BazToken"}
client = httpx.Client(transport=MockTransport(), headers=client_headers)
client = httpx.Client(transport=MockTransport(echo_headers), headers=client_headers)
response = client.get(url, headers=request_headers)
assert response.status_code == 200
@ -93,7 +79,7 @@ def test_header_merge_conflicting_headers():
def test_header_update():
url = "http://example.org/echo_headers"
client = httpx.Client(transport=MockTransport())
client = httpx.Client(transport=MockTransport(echo_headers))
first_response = client.get(url)
client.headers.update(
{"User-Agent": "python-myclient/0.2.1", "Another-Header": "AThing"}
@ -130,7 +116,7 @@ def test_remove_default_header():
"""
url = "http://example.org/echo_headers"
client = httpx.Client(transport=MockTransport())
client = httpx.Client(transport=MockTransport(echo_headers))
del client.headers["User-Agent"]
response = client.get(url)
@ -160,7 +146,7 @@ def test_host_with_auth_and_port_in_url():
"""
url = "http://username:password@example.org:80/echo_headers"
client = httpx.Client(transport=MockTransport())
client = httpx.Client(transport=MockTransport(echo_headers))
response = client.get(url)
assert response.status_code == 200
@ -183,7 +169,7 @@ def test_host_with_non_default_port_in_url():
"""
url = "http://username:password@example.org:123/echo_headers"
client = httpx.Client(transport=MockTransport())
client = httpx.Client(transport=MockTransport(echo_headers))
response = client.get(url)
assert response.status_code == 200

View File

@ -1,24 +1,9 @@
import typing
import httpcore
import httpx
from httpx._content_streams import ContentStream, JSONStream
from tests.utils import MockTransport
class MockTransport(httpcore.SyncHTTPTransport):
def request(
self,
method: bytes,
url: typing.Tuple[bytes, bytes, typing.Optional[int], bytes],
headers: typing.List[typing.Tuple[bytes, bytes]] = None,
stream: httpcore.SyncByteStream = None,
timeout: typing.Mapping[str, typing.Optional[float]] = None,
) -> typing.Tuple[
bytes, int, bytes, typing.List[typing.Tuple[bytes, bytes]], ContentStream
]:
body = JSONStream({"ok": "ok"})
return b"HTTP/1.1", 200, b"OK", [], body
def hello_world(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, content=b"Hello, world")
def test_client_queryparams():
@ -42,7 +27,9 @@ def test_client_queryparams_echo():
url = "http://example.org/echo_queryparams"
client_queryparams = "first=str"
request_queryparams = {"second": "dict"}
client = httpx.Client(transport=MockTransport(), params=client_queryparams)
client = httpx.Client(
transport=MockTransport(hello_world), params=client_queryparams
)
response = client.get(url, params=request_queryparams)
assert response.status_code == 200

View File

@ -1,180 +1,123 @@
import json
import typing
from urllib.parse import parse_qs
import httpcore
import pytest
import httpx
from httpx._content_streams import ByteStream, ContentStream, IteratorStream
from tests.utils import AsyncMockTransport, MockTransport
def get_header_value(headers, key, default=None):
lookup = key.encode("ascii").lower()
for header_key, header_value in headers:
if header_key.lower() == lookup:
return header_value.decode("ascii")
return default
def redirects(request: httpx.Request) -> httpx.Response:
if request.url.scheme not in ("http", "https"):
raise httpcore.UnsupportedProtocol(
f"Scheme {request.url.scheme!r} not supported."
)
if request.url.path == "/no_redirect":
return httpx.Response(200)
class MockTransport:
def _request(
self,
method: bytes,
url: typing.Tuple[bytes, bytes, int, bytes],
headers: typing.List[typing.Tuple[bytes, bytes]],
stream: ContentStream,
timeout: typing.Mapping[str, typing.Optional[float]] = None,
) -> typing.Tuple[
bytes, int, bytes, typing.List[typing.Tuple[bytes, bytes]], ContentStream
]:
scheme, host, port, path = url
if scheme not in (b"http", b"https"):
raise httpcore.UnsupportedProtocol(f"Scheme {scheme!r} not supported.")
elif request.url.path == "/redirect_301":
status_code = httpx.codes.MOVED_PERMANENTLY
content = b"<a href='https://example.org/'>here</a>"
headers = {"location": "https://example.org/"}
return httpx.Response(status_code, headers=headers, content=content)
path, _, query = path.partition(b"?")
if path == b"/no_redirect":
return b"HTTP/1.1", httpx.codes.OK, b"OK", [], ByteStream(b"")
elif request.url.path == "/redirect_302":
status_code = httpx.codes.FOUND
headers = {"location": "https://example.org/"}
return httpx.Response(status_code, headers=headers)
elif path == b"/redirect_301":
elif request.url.path == "/redirect_303":
status_code = httpx.codes.SEE_OTHER
headers = {"location": "https://example.org/"}
return httpx.Response(status_code, headers=headers)
def body():
yield b"<a href='https://example.org/'>here</a>"
elif request.url.path == "/relative_redirect":
status_code = httpx.codes.SEE_OTHER
headers = {"location": "/"}
return httpx.Response(status_code, headers=headers)
status_code = httpx.codes.MOVED_PERMANENTLY
headers = [(b"location", b"https://example.org/")]
stream = IteratorStream(iterator=body())
return b"HTTP/1.1", status_code, b"Moved Permanently", headers, stream
elif request.url.path == "/malformed_redirect":
status_code = httpx.codes.SEE_OTHER
headers = {"location": "https://:443/"}
return httpx.Response(status_code, headers=headers)
elif path == b"/redirect_302":
status_code = httpx.codes.FOUND
headers = [(b"location", b"https://example.org/")]
return b"HTTP/1.1", status_code, b"Found", headers, ByteStream(b"")
elif request.url.path == "/invalid_redirect":
status_code = httpx.codes.SEE_OTHER
raw_headers = [(b"location", "https://😇/".encode("utf-8"))]
return httpx.Response(status_code, headers=raw_headers)
elif path == b"/redirect_303":
status_code = httpx.codes.SEE_OTHER
headers = [(b"location", b"https://example.org/")]
return b"HTTP/1.1", status_code, b"See Other", headers, ByteStream(b"")
elif request.url.path == "/no_scheme_redirect":
status_code = httpx.codes.SEE_OTHER
headers = {"location": "//example.org/"}
return httpx.Response(status_code, headers=headers)
elif path == b"/relative_redirect":
status_code = httpx.codes.SEE_OTHER
headers = [(b"location", b"/")]
return b"HTTP/1.1", status_code, b"See Other", headers, ByteStream(b"")
elif path == b"/malformed_redirect":
status_code = httpx.codes.SEE_OTHER
headers = [(b"location", b"https://:443/")]
return b"HTTP/1.1", status_code, b"See Other", headers, ByteStream(b"")
elif path == b"/invalid_redirect":
status_code = httpx.codes.SEE_OTHER
headers = [(b"location", "https://😇/".encode("utf-8"))]
return b"HTTP/1.1", status_code, b"See Other", headers, ByteStream(b"")
elif path == b"/no_scheme_redirect":
status_code = httpx.codes.SEE_OTHER
headers = [(b"location", b"//example.org/")]
return b"HTTP/1.1", status_code, b"See Other", headers, ByteStream(b"")
elif path == b"/multiple_redirects":
params = parse_qs(query.decode("ascii"))
count = int(params.get("count", "0")[0])
redirect_count = count - 1
code = httpx.codes.SEE_OTHER if count else httpx.codes.OK
phrase = b"See Other" if count else b"OK"
location = b"/multiple_redirects"
elif request.url.path == "/multiple_redirects":
params = httpx.QueryParams(request.url.query)
count = int(params.get("count", "0"))
redirect_count = count - 1
status_code = httpx.codes.SEE_OTHER if count else httpx.codes.OK
if count:
location = "/multiple_redirects"
if redirect_count:
location += b"?count=" + str(redirect_count).encode("ascii")
headers = [(b"location", location)] if count else []
return b"HTTP/1.1", code, phrase, headers, ByteStream(b"")
location += f"?count={redirect_count}"
headers = {"location": location}
else:
headers = {}
return httpx.Response(status_code, headers=headers)
if path == b"/redirect_loop":
code = httpx.codes.SEE_OTHER
headers = [(b"location", b"/redirect_loop")]
return b"HTTP/1.1", code, b"See Other", headers, ByteStream(b"")
if request.url.path == "/redirect_loop":
status_code = httpx.codes.SEE_OTHER
headers = {"location": "/redirect_loop"}
return httpx.Response(status_code, headers=headers)
elif path == b"/cross_domain":
code = httpx.codes.SEE_OTHER
headers = [(b"location", b"https://example.org/cross_domain_target")]
return b"HTTP/1.1", code, b"See Other", headers, ByteStream(b"")
elif request.url.path == "/cross_domain":
status_code = httpx.codes.SEE_OTHER
headers = {"location": "https://example.org/cross_domain_target"}
return httpx.Response(status_code, headers=headers)
elif path == b"/cross_domain_target":
headers_dict = {
key.decode("ascii"): value.decode("ascii") for key, value in headers
}
stream = ByteStream(json.dumps({"headers": headers_dict}).encode())
return b"HTTP/1.1", 200, b"OK", [], stream
elif request.url.path == "/cross_domain_target":
status_code = httpx.codes.OK
content = json.dumps({"headers": dict(request.headers)}).encode("utf-8")
return httpx.Response(status_code, content=content)
elif path == b"/redirect_body":
code = httpx.codes.PERMANENT_REDIRECT
headers = [(b"location", b"/redirect_body_target")]
return b"HTTP/1.1", code, b"Permanent Redirect", headers, ByteStream(b"")
elif request.url.path == "/redirect_body":
status_code = httpx.codes.PERMANENT_REDIRECT
headers = {"location": "/redirect_body_target"}
return httpx.Response(status_code, headers=headers)
elif path == b"/redirect_no_body":
code = httpx.codes.SEE_OTHER
headers = [(b"location", b"/redirect_body_target")]
return b"HTTP/1.1", code, b"See Other", headers, ByteStream(b"")
elif request.url.path == "/redirect_no_body":
status_code = httpx.codes.SEE_OTHER
headers = {"location": "/redirect_body_target"}
return httpx.Response(status_code, headers=headers)
elif path == b"/redirect_body_target":
content = b"".join(stream)
headers_dict = {
key.decode("ascii"): value.decode("ascii") for key, value in headers
}
stream = ByteStream(
json.dumps({"body": content.decode(), "headers": headers_dict}).encode()
)
return b"HTTP/1.1", 200, b"OK", [], stream
elif request.url.path == "/redirect_body_target":
content = json.dumps(
{"body": request.content.decode("ascii"), "headers": dict(request.headers)}
).encode("utf-8")
return httpx.Response(200, content=content)
elif path == b"/cross_subdomain":
host = get_header_value(headers, "host")
if host != "www.example.org":
headers = [(b"location", b"https://www.example.org/cross_subdomain")]
return (
b"HTTP/1.1",
httpx.codes.PERMANENT_REDIRECT,
b"Permanent Redirect",
headers,
ByteStream(b""),
)
else:
return b"HTTP/1.1", 200, b"OK", [], ByteStream(b"Hello, world!")
elif request.url.path == "/cross_subdomain":
if request.headers["Host"] != "www.example.org":
status_code = httpx.codes.PERMANENT_REDIRECT
headers = {"location": "https://www.example.org/cross_subdomain"}
return httpx.Response(status_code, headers=headers)
else:
return httpx.Response(200, content=b"Hello, world!")
elif path == b"/redirect_custom_scheme":
status_code = httpx.codes.MOVED_PERMANENTLY
headers = [(b"location", b"market://details?id=42")]
return (
b"HTTP/1.1",
status_code,
b"Moved Permanently",
headers,
ByteStream(b""),
)
elif request.url.path == "/redirect_custom_scheme":
status_code = httpx.codes.MOVED_PERMANENTLY
headers = {"location": "market://details?id=42"}
return httpx.Response(status_code, headers=headers)
stream = ByteStream(b"Hello, world!") if method != b"HEAD" else ByteStream(b"")
if request.method == "HEAD":
return httpx.Response(200)
return b"HTTP/1.1", 200, b"OK", [], stream
class AsyncMockTransport(MockTransport, httpcore.AsyncHTTPTransport):
async def request(
self, *args, **kwargs
) -> typing.Tuple[
bytes, int, bytes, typing.List[typing.Tuple[bytes, bytes]], ContentStream
]:
return self._request(*args, **kwargs)
class SyncMockTransport(MockTransport, httpcore.SyncHTTPTransport):
def request(
self, *args, **kwargs
) -> typing.Tuple[
bytes, int, bytes, typing.List[typing.Tuple[bytes, bytes]], ContentStream
]:
return self._request(*args, **kwargs)
return httpx.Response(200, content=b"Hello, world!")
def test_no_redirect():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
url = "https://example.com/no_redirect"
response = client.get(url)
assert response.status_code == 200
@ -183,7 +126,7 @@ def test_no_redirect():
def test_redirect_301():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
response = client.post("https://example.org/redirect_301")
assert response.status_code == httpx.codes.OK
assert response.url == "https://example.org/"
@ -191,7 +134,7 @@ def test_redirect_301():
def test_redirect_302():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
response = client.post("https://example.org/redirect_302")
assert response.status_code == httpx.codes.OK
assert response.url == "https://example.org/"
@ -199,7 +142,7 @@ def test_redirect_302():
def test_redirect_303():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
response = client.get("https://example.org/redirect_303")
assert response.status_code == httpx.codes.OK
assert response.url == "https://example.org/"
@ -207,7 +150,7 @@ def test_redirect_303():
def test_disallow_redirects():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
response = client.post("https://example.org/redirect_303", allow_redirects=False)
assert response.status_code == httpx.codes.SEE_OTHER
assert response.url == "https://example.org/redirect_303"
@ -225,7 +168,7 @@ def test_head_redirect():
"""
Contrary to Requests, redirects remain enabled by default for HEAD requests.
"""
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
response = client.head("https://example.org/redirect_302")
assert response.status_code == httpx.codes.OK
assert response.url == "https://example.org/"
@ -235,7 +178,7 @@ def test_head_redirect():
def test_relative_redirect():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
response = client.get("https://example.org/relative_redirect")
assert response.status_code == httpx.codes.OK
assert response.url == "https://example.org/"
@ -244,7 +187,7 @@ def test_relative_redirect():
def test_malformed_redirect():
# https://github.com/encode/httpx/issues/771
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
response = client.get("http://example.org/malformed_redirect")
assert response.status_code == httpx.codes.OK
assert response.url == "https://example.org:443/"
@ -252,13 +195,13 @@ def test_malformed_redirect():
def test_invalid_redirect():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
with pytest.raises(httpx.RemoteProtocolError):
client.get("http://example.org/invalid_redirect")
def test_no_scheme_redirect():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
response = client.get("https://example.org/no_scheme_redirect")
assert response.status_code == httpx.codes.OK
assert response.url == "https://example.org/"
@ -266,7 +209,7 @@ def test_no_scheme_redirect():
def test_fragment_redirect():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
response = client.get("https://example.org/relative_redirect#fragment")
assert response.status_code == httpx.codes.OK
assert response.url == "https://example.org/#fragment"
@ -274,7 +217,7 @@ def test_fragment_redirect():
def test_multiple_redirects():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
response = client.get("https://example.org/multiple_redirects?count=20")
assert response.status_code == httpx.codes.OK
assert response.url == "https://example.org/multiple_redirects"
@ -287,14 +230,14 @@ def test_multiple_redirects():
@pytest.mark.usefixtures("async_environment")
async def test_async_too_many_redirects():
async with httpx.AsyncClient(transport=AsyncMockTransport()) as client:
async with httpx.AsyncClient(transport=AsyncMockTransport(redirects)) as client:
with pytest.raises(httpx.TooManyRedirects):
await client.get("https://example.org/multiple_redirects?count=21")
@pytest.mark.usefixtures("async_environment")
async def test_async_too_many_redirects_calling_next():
async with httpx.AsyncClient(transport=AsyncMockTransport()) as client:
async with httpx.AsyncClient(transport=AsyncMockTransport(redirects)) as client:
url = "https://example.org/multiple_redirects?count=21"
response = await client.get(url, allow_redirects=False)
with pytest.raises(httpx.TooManyRedirects):
@ -303,13 +246,13 @@ async def test_async_too_many_redirects_calling_next():
def test_sync_too_many_redirects():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
with pytest.raises(httpx.TooManyRedirects):
client.get("https://example.org/multiple_redirects?count=21")
def test_sync_too_many_redirects_calling_next():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
url = "https://example.org/multiple_redirects?count=21"
response = client.get(url, allow_redirects=False)
with pytest.raises(httpx.TooManyRedirects):
@ -318,13 +261,13 @@ def test_sync_too_many_redirects_calling_next():
def test_redirect_loop():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
with pytest.raises(httpx.TooManyRedirects):
client.get("https://example.org/redirect_loop")
def test_cross_domain_redirect_with_auth_header():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
url = "https://example.com/cross_domain"
headers = {"Authorization": "abc"}
response = client.get(url, headers=headers)
@ -333,7 +276,7 @@ def test_cross_domain_redirect_with_auth_header():
def test_cross_domain_redirect_with_auth():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
url = "https://example.com/cross_domain"
response = client.get(url, auth=("user", "pass"))
assert response.url == "https://example.org/cross_domain_target"
@ -341,7 +284,7 @@ def test_cross_domain_redirect_with_auth():
def test_same_domain_redirect():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
url = "https://example.org/cross_domain"
headers = {"Authorization": "abc"}
response = client.get(url, headers=headers)
@ -353,7 +296,7 @@ def test_body_redirect():
"""
A 308 redirect should preserve the request body.
"""
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
url = "https://example.org/redirect_body"
data = b"Example request body"
response = client.post(url, data=data)
@ -366,7 +309,7 @@ def test_no_body_redirect():
"""
A 303 redirect should remove the request body.
"""
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
url = "https://example.org/redirect_no_body"
data = b"Example request body"
response = client.post(url, data=data)
@ -376,7 +319,7 @@ def test_no_body_redirect():
def test_can_stream_if_no_redirect():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
url = "https://example.org/redirect_301"
with client.stream("GET", url, allow_redirects=False) as response:
assert not response.is_closed
@ -385,7 +328,7 @@ def test_can_stream_if_no_redirect():
def test_cannot_redirect_streaming_body():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
url = "https://example.org/redirect_body"
def streaming_body():
@ -396,64 +339,47 @@ def test_cannot_redirect_streaming_body():
def test_cross_subdomain_redirect():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
url = "https://example.com/cross_subdomain"
response = client.get(url)
assert response.url == "https://www.example.org/cross_subdomain"
class MockCookieTransport(httpcore.SyncHTTPTransport):
def request(
self,
method: bytes,
url: typing.Tuple[bytes, bytes, typing.Optional[int], bytes],
headers: typing.List[typing.Tuple[bytes, bytes]] = None,
stream: httpcore.SyncByteStream = None,
timeout: typing.Mapping[str, typing.Optional[float]] = None,
) -> typing.Tuple[
bytes, int, bytes, typing.List[typing.Tuple[bytes, bytes]], ContentStream
]:
scheme, host, port, path = url
if path == b"/":
cookie = get_header_value(headers, "Cookie")
if cookie is not None:
content = b"Logged in"
else:
content = b"Not logged in"
return b"HTTP/1.1", 200, b"OK", [], ByteStream(content)
elif path == b"/login":
status_code = httpx.codes.SEE_OTHER
headers = [
(b"location", b"/"),
(
b"set-cookie",
(
b"session=eyJ1c2VybmFtZSI6ICJ0b21; path=/; Max-Age=1209600; "
b"httponly; samesite=lax"
),
),
]
return b"HTTP/1.1", status_code, b"See Other", headers, ByteStream(b"")
def cookie_sessions(request: httpx.Request) -> httpx.Response:
if request.url.path == "/":
cookie = request.headers.get("Cookie")
if cookie is not None:
content = b"Logged in"
else:
assert path == b"/logout"
status_code = httpx.codes.SEE_OTHER
headers = [
(b"location", b"/"),
(
b"set-cookie",
(
b"session=null; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; "
b"httponly; samesite=lax"
),
),
]
return b"HTTP/1.1", status_code, b"See Other", headers, ByteStream(b"")
content = b"Not logged in"
return httpx.Response(200, content=content)
elif request.url.path == "/login":
status_code = httpx.codes.SEE_OTHER
headers = {
"location": "/",
"set-cookie": (
"session=eyJ1c2VybmFtZSI6ICJ0b21; path=/; Max-Age=1209600; "
"httponly; samesite=lax"
),
}
return httpx.Response(status_code, headers=headers)
else:
assert request.url.path == "/logout"
status_code = httpx.codes.SEE_OTHER
headers = {
"location": "/",
"set-cookie": (
"session=null; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; "
"httponly; samesite=lax"
),
}
return httpx.Response(status_code, headers=headers)
def test_redirect_cookie_behavior():
client = httpx.Client(transport=MockCookieTransport())
client = httpx.Client(transport=MockTransport(cookie_sessions))
# The client is not logged in.
response = client.get("https://example.com/")
@ -482,7 +408,7 @@ def test_redirect_cookie_behavior():
def test_redirect_custom_scheme():
client = httpx.Client(transport=SyncMockTransport())
client = httpx.Client(transport=MockTransport(redirects))
with pytest.raises(httpx.UnsupportedProtocol) as e:
client.post("https://example.org/redirect_custom_scheme")
assert str(e.value) == "Scheme b'market' not supported."
assert str(e.value) == "Scheme 'market' not supported."

View File

@ -4,37 +4,21 @@ import os
import typing
from unittest import mock
import httpcore
import pytest
import httpx
from httpx._content_streams import MultipartStream, encode
from httpx._utils import format_form_param
from tests.utils import MockTransport
class MockTransport(httpcore.SyncHTTPTransport):
def request(
self,
method: bytes,
url: typing.Tuple[bytes, bytes, typing.Optional[int], bytes],
headers: typing.List[typing.Tuple[bytes, bytes]] = None,
stream: httpcore.SyncByteStream = None,
timeout: typing.Mapping[str, typing.Optional[float]] = None,
) -> typing.Tuple[
bytes,
int,
bytes,
typing.List[typing.Tuple[bytes, bytes]],
httpcore.SyncByteStream,
]:
assert stream is not None
content = httpcore.IteratorByteStream(iterator=(part for part in stream))
return b"HTTP/1.1", 200, b"OK", [], content
def echo_request_content(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, content=request.content)
@pytest.mark.parametrize(("value,output"), (("abc", b"abc"), (b"abc", b"abc")))
def test_multipart(value, output):
client = httpx.Client(transport=MockTransport())
client = httpx.Client(transport=MockTransport(echo_request_content))
# Test with a single-value 'data' argument, and a plain file 'files' argument.
data = {"text": value}
@ -60,7 +44,7 @@ def test_multipart(value, output):
@pytest.mark.parametrize(("key"), (b"abc", 1, 2.3, None))
def test_multipart_invalid_key(key):
client = httpx.Client(transport=MockTransport())
client = httpx.Client(transport=MockTransport(echo_request_content))
data = {key: "abc"}
files = {"file": io.BytesIO(b"<file content>")}
@ -75,7 +59,7 @@ def test_multipart_invalid_key(key):
@pytest.mark.parametrize(("value"), (1, 2.3, None, [None, "abc"], {None: "abc"}))
def test_multipart_invalid_value(value):
client = httpx.Client(transport=MockTransport())
client = httpx.Client(transport=MockTransport(echo_request_content))
data = {"text": value}
files = {"file": io.BytesIO(b"<file content>")}
@ -85,7 +69,7 @@ def test_multipart_invalid_value(value):
def test_multipart_file_tuple():
client = httpx.Client(transport=MockTransport())
client = httpx.Client(transport=MockTransport(echo_request_content))
# Test with a list of values 'data' argument,
# and a tuple style 'files' argument.

View File

@ -1,7 +1,11 @@
import contextlib
import logging
import os
from typing import Callable, List, Mapping, Optional, Tuple
import httpcore
import httpx
from httpx import _utils
@ -18,3 +22,90 @@ def override_log_level(log_level: str):
finally:
# Reset the logger so we don't have verbose output in all unit tests
logging.getLogger("httpx").handlers = []
class MockTransport(httpcore.SyncHTTPTransport):
def __init__(self, handler: Callable) -> None:
self.handler = handler
def request(
self,
method: bytes,
url: Tuple[bytes, bytes, Optional[int], bytes],
headers: List[Tuple[bytes, bytes]] = None,
stream: httpcore.SyncByteStream = None,
timeout: Mapping[str, Optional[float]] = None,
) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], httpcore.SyncByteStream]:
raw_scheme, raw_host, port, raw_path = url
scheme = raw_scheme.decode("ascii")
host = raw_host.decode("ascii")
port_str = "" if port is None else f":{port}"
path = raw_path.decode("ascii")
request_headers = httpx.Headers(headers)
data = (
(item for item in stream)
if stream
and (
"Content-Length" in request_headers
or "Transfer-Encoding" in request_headers
)
else None
)
request = httpx.Request(
method=method.decode("ascii"),
url=f"{scheme}://{host}{port_str}{path}",
headers=request_headers,
data=data,
)
request.read()
response = self.handler(request)
return (
response.http_version.encode("ascii")
if response.http_version
else b"HTTP/1.1",
response.status_code,
response.reason_phrase.encode("ascii"),
response.headers.raw,
response._raw_stream,
)
class AsyncMockTransport(httpcore.AsyncHTTPTransport):
def __init__(self, handler: Callable) -> None:
self.impl = MockTransport(handler)
async def request(
self,
method: bytes,
url: Tuple[bytes, bytes, Optional[int], bytes],
headers: List[Tuple[bytes, bytes]] = None,
stream: httpcore.AsyncByteStream = None,
timeout: Mapping[str, Optional[float]] = None,
) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], httpcore.AsyncByteStream]:
content = (
httpcore.PlainByteStream(b"".join([part async for part in stream]))
if stream
else httpcore.PlainByteStream(b"")
)
(
http_version,
status_code,
reason_phrase,
headers,
response_stream,
) = self.impl.request(
method, url, headers=headers, stream=content, timeout=timeout
)
content = httpcore.PlainByteStream(b"".join([part for part in response_stream]))
return (
http_version,
status_code,
reason_phrase,
headers,
content,
)