]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Increase test coverage - take 2 (#1012)
authorJosep Cugat <jcugat@gmail.com>
Tue, 2 Jun 2020 09:24:45 +0000 (11:24 +0200)
committerGitHub <noreply@github.com>
Tue, 2 Jun 2020 09:24:45 +0000 (10:24 +0100)
* Fix HttpError -> HTTPError typo

* Increased test coverage

* Increase coverage threshold

* Reuse sync/async transport code in test_auth.py

* Removed close_client check inside StreamContextManager

It's never set as True when used in async

* Reuse sync/async transport code in test_redirects.py

13 files changed:
docs/quickstart.md
httpx/_client.py
httpx/_models.py
scripts/coverage
tests/client/test_auth.py
tests/client/test_proxies.py
tests/client/test_redirects.py
tests/concurrency.py
tests/conftest.py
tests/models/test_url.py
tests/test_asgi.py
tests/test_utils.py
tests/test_wsgi.py

index 3d67636f17ea05f516145ffa7f9223d6e2d50fe1..504822b605d07c04a4b625902d5598165b1cf6f4 100644 (file)
@@ -249,8 +249,8 @@ We can raise an exception for any Client or Server error responses (4xx or 5xx s
 >>> not_found.raise_for_status()
 Traceback (most recent call last):
   File "/Users/tomchristie/GitHub/encode/httpcore/httpx/models.py", line 776, in raise_for_status
-    raise HttpError(message)
-httpx.exceptions.HttpError: 404 Not Found
+    raise HTTPError(message)
+httpx.HTTPError: 404 Not Found
 ```
 
 Any successful response codes will simply return `None` rather than raising an exception.
index 748c3b65c286f1f8a90fb7dd90d215eaae921485..c2a485fdf509acd73040a31971653a23e4823e49 100644 (file)
@@ -1539,5 +1539,3 @@ class StreamContextManager:
     ) -> None:
         assert isinstance(self.client, AsyncClient)
         await self.response.aclose()
-        if self.close_client:
-            await self.client.aclose()
index f11d53fd06d98992eff1e95cd90c5c9889613ea3..230ae8ffbf3c7e0da100c97627959c208363e901 100644 (file)
@@ -826,7 +826,7 @@ class Response:
 
     def raise_for_status(self) -> None:
         """
-        Raise the `HttpError` if one occurred.
+        Raise the `HTTPError` if one occurred.
         """
         message = (
             "{0.status_code} {error_type}: {0.reason_phrase} for url: {0.url}\n"
index 68654daf600c1aa7d13b0d98af50d985247a67c1..ab476b35c243c7329762f4dadd929913160596ad 100755 (executable)
@@ -8,4 +8,4 @@ export SOURCE_FILES="httpx tests"
 
 set -x
 
-${PREFIX}coverage report --show-missing --skip-covered --fail-under=97
+${PREFIX}coverage report --show-missing --skip-covered --fail-under=99
index 78a19534c2d8a8ce10822f5067ba39b153dca7da..818e65904a606c22f1e3af4490874b91d90c33e1 100644 (file)
@@ -9,6 +9,7 @@ from httpx import (
     URL,
     AsyncClient,
     Auth,
+    Client,
     DigestAuth,
     Headers,
     ProtocolError,
@@ -27,12 +28,12 @@ def get_header_value(headers, key, default=None):
     return default
 
 
-class MockTransport(httpcore.AsyncHTTPTransport):
+class MockTransport:
     def __init__(self, auth_header: bytes = b"", status_code: int = 200) -> None:
         self.auth_header = auth_header
         self.status_code = status_code
 
-    async def request(
+    def _request(
         self,
         method: bytes,
         url: typing.Tuple[bytes, bytes, int, bytes],
@@ -50,6 +51,24 @@ class MockTransport(httpcore.AsyncHTTPTransport):
         return b"HTTP/1.1", self.status_code, b"", response_headers, response_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)
+
+
 class MockDigestAuthTransport(httpcore.AsyncHTTPTransport):
     def __init__(
         self,
@@ -114,12 +133,58 @@ class MockDigestAuthTransport(httpcore.AsyncHTTPTransport):
         return b"HTTP/1.1", 401, b"", headers, ContentStream()
 
 
+class RepeatAuth(Auth):
+    """
+    A mock authentication scheme that requires clients to send
+    the request a fixed number of times, and then send a last request containing
+    an aggregation of nonces that the server sent in 'WWW-Authenticate' headers
+    of intermediate responses.
+    """
+
+    requires_request_body = True
+
+    def __init__(self, repeat: int):
+        self.repeat = repeat
+
+    def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
+        nonces = []
+
+        for index in range(self.repeat):
+            request.headers["Authorization"] = f"Repeat {index}"
+            response = yield request
+            nonces.append(response.headers["www-authenticate"])
+
+        key = ".".join(nonces)
+        request.headers["Authorization"] = f"Repeat {key}"
+        yield request
+
+
+class ResponseBodyAuth(Auth):
+    """
+    A mock authentication scheme that requires clients to send an 'Authorization'
+    header, then send back the contents of the response in the 'Authorization'
+    header.
+    """
+
+    requires_response_body = True
+
+    def __init__(self, token):
+        self.token = token
+
+    def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
+        request.headers["Authorization"] = self.token
+        response = yield request
+        data = response.text
+        request.headers["Authorization"] = data
+        yield request
+
+
 @pytest.mark.asyncio
 async def test_basic_auth() -> None:
     url = "https://example.org/"
     auth = ("tomchristie", "password123")
 
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     response = await client.get(url, auth=auth)
 
     assert response.status_code == 200
@@ -130,7 +195,7 @@ async def test_basic_auth() -> None:
 async def test_basic_auth_in_url() -> None:
     url = "https://tomchristie:password123@example.org/"
 
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     response = await client.get(url)
 
     assert response.status_code == 200
@@ -142,7 +207,7 @@ async def test_basic_auth_on_session() -> None:
     url = "https://example.org/"
     auth = ("tomchristie", "password123")
 
-    client = AsyncClient(transport=MockTransport(), auth=auth)
+    client = AsyncClient(transport=AsyncMockTransport(), auth=auth)
     response = await client.get(url)
 
     assert response.status_code == 200
@@ -157,7 +222,7 @@ async def test_custom_auth() -> None:
         request.headers["Authorization"] = "Token 123"
         return request
 
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     response = await client.get(url, auth=auth)
 
     assert response.status_code == 200
@@ -169,7 +234,7 @@ async def test_netrc_auth() -> None:
     os.environ["NETRC"] = "tests/.netrc"
     url = "http://netrcexample.org"
 
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     response = await client.get(url)
 
     assert response.status_code == 200
@@ -183,7 +248,7 @@ async def test_auth_header_has_priority_over_netrc() -> None:
     os.environ["NETRC"] = "tests/.netrc"
     url = "http://netrcexample.org"
 
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     response = await client.get(url, headers={"Authorization": "Override"})
 
     assert response.status_code == 200
@@ -195,13 +260,13 @@ async def test_trust_env_auth() -> None:
     os.environ["NETRC"] = "tests/.netrc"
     url = "http://netrcexample.org"
 
-    client = AsyncClient(transport=MockTransport(), trust_env=False)
+    client = AsyncClient(transport=AsyncMockTransport(), trust_env=False)
     response = await client.get(url)
 
     assert response.status_code == 200
     assert response.json() == {"auth": None}
 
-    client = AsyncClient(transport=MockTransport(), trust_env=True)
+    client = AsyncClient(transport=AsyncMockTransport(), trust_env=True)
     response = await client.get(url)
 
     assert response.status_code == 200
@@ -222,7 +287,7 @@ async def test_auth_hidden_header() -> None:
     url = "https://example.org/"
     auth = ("example-username", "example-password")
 
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     response = await client.get(url, auth=auth)
 
     assert "'authorization': '[secure]'" in str(response.request.headers)
@@ -232,7 +297,7 @@ async def test_auth_hidden_header() -> None:
 async def test_auth_invalid_type() -> None:
     url = "https://example.org/"
     client = AsyncClient(
-        transport=MockTransport(), auth="not a tuple, not a callable",  # type: ignore
+        transport=AsyncMockTransport(), auth="not a tuple, not a callable",
     )
     with pytest.raises(TypeError):
         await client.get(url)
@@ -243,7 +308,7 @@ async def test_digest_auth_returns_no_auth_if_no_digest_header_in_response() ->
     url = "https://example.org/"
     auth = DigestAuth(username="tomchristie", password="password123")
 
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     response = await client.get(url, auth=auth)
 
     assert response.status_code == 200
@@ -258,7 +323,7 @@ async def test_digest_auth_200_response_including_digest_auth_header() -> None:
     auth_header = b'Digest realm="realm@host.com",qop="auth",nonce="abc",opaque="xyz"'
 
     client = AsyncClient(
-        transport=MockTransport(auth_header=auth_header, status_code=200)
+        transport=AsyncMockTransport(auth_header=auth_header, status_code=200)
     )
     response = await client.get(url, auth=auth)
 
@@ -272,7 +337,7 @@ async def test_digest_auth_401_response_without_digest_auth_header() -> None:
     url = "https://example.org/"
     auth = DigestAuth(username="tomchristie", password="password123")
 
-    client = AsyncClient(transport=MockTransport(auth_header=b"", status_code=401))
+    client = AsyncClient(transport=AsyncMockTransport(auth_header=b"", status_code=401))
     response = await client.get(url, auth=auth)
 
     assert response.status_code == 401
@@ -413,58 +478,77 @@ async def test_digest_auth_incorrect_credentials() -> None:
     ],
 )
 @pytest.mark.asyncio
-async def test_digest_auth_raises_protocol_error_on_malformed_header(
+async def test_async_digest_auth_raises_protocol_error_on_malformed_header(
     auth_header: bytes,
 ) -> None:
     url = "https://example.org/"
     auth = DigestAuth(username="tomchristie", password="password123")
     client = AsyncClient(
-        transport=MockTransport(auth_header=auth_header, status_code=401)
+        transport=AsyncMockTransport(auth_header=auth_header, status_code=401)
     )
 
     with pytest.raises(ProtocolError):
         await client.get(url, auth=auth)
 
 
+@pytest.mark.parametrize(
+    "auth_header",
+    [
+        b'Digest realm="httpx@example.org", qop="auth"',  # missing fields
+        b'realm="httpx@example.org", qop="auth"',  # not starting with Digest
+        b'DigestZ realm="httpx@example.org", qop="auth"'
+        b'qop="auth,auth-int",nonce="abc",opaque="xyz"',
+        b'Digest realm="httpx@example.org", qop="auth,au',  # malformed fields list
+    ],
+)
+def test_sync_digest_auth_raises_protocol_error_on_malformed_header(
+    auth_header: bytes,
+) -> None:
+    url = "https://example.org/"
+    auth = DigestAuth(username="tomchristie", password="password123")
+    client = Client(
+        transport=SyncMockTransport(auth_header=auth_header, status_code=401)
+    )
+
+    with pytest.raises(ProtocolError):
+        client.get(url, auth=auth)
+
+
 @pytest.mark.asyncio
-async def test_auth_history() -> None:
+async def test_async_auth_history() -> None:
     """
     Test that intermediate requests sent as part of an authentication flow
     are recorded in the response history.
     """
+    url = "https://example.org/"
+    auth = RepeatAuth(repeat=2)
+    client = AsyncClient(transport=AsyncMockTransport(auth_header=b"abc"))
 
-    class RepeatAuth(Auth):
-        """
-        A mock authentication scheme that requires clients to send
-        the request a fixed number of times, and then send a last request containing
-        an aggregation of nonces that the server sent in 'WWW-Authenticate' headers
-        of intermediate responses.
-        """
-
-        requires_request_body = True
+    response = await client.get(url, auth=auth)
+    assert response.status_code == 200
+    assert response.json() == {"auth": "Repeat abc.abc"}
 
-        def __init__(self, repeat: int):
-            self.repeat = repeat
+    assert len(response.history) == 2
+    resp1, resp2 = response.history
+    assert resp1.json() == {"auth": "Repeat 0"}
+    assert resp2.json() == {"auth": "Repeat 1"}
 
-        def auth_flow(
-            self, request: Request
-        ) -> typing.Generator[Request, Response, None]:
-            nonces = []
+    assert len(resp2.history) == 1
+    assert resp2.history == [resp1]
 
-            for index in range(self.repeat):
-                request.headers["Authorization"] = f"Repeat {index}"
-                response = yield request
-                nonces.append(response.headers["www-authenticate"])
+    assert len(resp1.history) == 0
 
-            key = ".".join(nonces)
-            request.headers["Authorization"] = f"Repeat {key}"
-            yield request
 
+def test_sync_auth_history() -> None:
+    """
+    Test that intermediate requests sent as part of an authentication flow
+    are recorded in the response history.
+    """
     url = "https://example.org/"
     auth = RepeatAuth(repeat=2)
-    client = AsyncClient(transport=MockTransport(auth_header=b"abc"))
+    client = Client(transport=SyncMockTransport(auth_header=b"abc"))
 
-    response = await client.get(url, auth=auth)
+    response = client.get(url, auth=auth)
     assert response.status_code == 200
     assert response.json() == {"auth": "Repeat abc.abc"}
 
@@ -483,7 +567,7 @@ async def test_auth_history() -> None:
 async def test_digest_auth_unavailable_streaming_body():
     url = "https://example.org/"
     auth = DigestAuth(username="tomchristie", password="password123")
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
 
     async def streaming_body():
         yield b"Example request body"  # pragma: nocover
@@ -493,37 +577,29 @@ async def test_digest_auth_unavailable_streaming_body():
 
 
 @pytest.mark.asyncio
-async def test_auth_reads_response_body() -> None:
+async def test_async_auth_reads_response_body() -> None:
     """
     Test that we can read the response body in an auth flow if `requires_response_body`
     is set.
     """
+    url = "https://example.org/"
+    auth = ResponseBodyAuth("xyz")
+    client = AsyncClient(transport=AsyncMockTransport())
 
-    class ResponseBodyAuth(Auth):
-        """
-        A mock authentication scheme that requires clients to send an 'Authorization'
-        header, then send back the contents of the response in the 'Authorization'
-        header.
-        """
-
-        requires_response_body = True
-
-        def __init__(self, token):
-            self.token = token
+    response = await client.get(url, auth=auth)
+    assert response.status_code == 200
+    assert response.json() == {"auth": '{"auth": "xyz"}'}
 
-        def auth_flow(
-            self, request: Request
-        ) -> typing.Generator[Request, Response, None]:
-            request.headers["Authorization"] = self.token
-            response = yield request
-            data = response.text
-            request.headers["Authorization"] = data
-            yield request
 
+def test_sync_auth_reads_response_body() -> None:
+    """
+    Test that we can read the response body in an auth flow if `requires_response_body`
+    is set.
+    """
     url = "https://example.org/"
     auth = ResponseBodyAuth("xyz")
-    client = AsyncClient(transport=MockTransport())
+    client = Client(transport=SyncMockTransport())
 
-    response = await client.get(url, auth=auth)
+    response = client.get(url, auth=auth)
     assert response.status_code == 200
     assert response.json() == {"auth": '{"auth": "xyz"}'}
index 1b6253b2a22489b8b00df3c117c4591bb0591da1..5222b08e342e59ce8588b7e9fa34641220986692 100644 (file)
@@ -84,6 +84,17 @@ def test_transport_for_request(url, proxies, expected):
         assert transport.proxy_origin == httpx.URL(expected).raw[:3]
 
 
+@pytest.mark.asyncio
+async def test_async_proxy_close():
+    client = httpx.AsyncClient(proxies={"all": PROXY_URL})
+    await client.aclose()
+
+
+def test_sync_proxy_close():
+    client = httpx.Client(proxies={"all": PROXY_URL})
+    client.close()
+
+
 def test_unsupported_proxy_scheme():
     with pytest.raises(ValueError):
         httpx.AsyncClient(proxies="ftp://127.0.0.1")
index 50ddf8b9ec63ffb9b04ed42ba98a7e329d977db2..b39e8d0e9e02e058835ffda156dc3afcdf387705 100644 (file)
@@ -8,6 +8,7 @@ import pytest
 from httpx import (
     URL,
     AsyncClient,
+    Client,
     InvalidURL,
     NotRedirectResponse,
     RequestBodyUnavailable,
@@ -25,8 +26,8 @@ def get_header_value(headers, key, default=None):
     return default
 
 
-class MockTransport(httpcore.AsyncHTTPTransport):
-    async def request(
+class MockTransport:
+    def _request(
         self,
         method: bytes,
         url: typing.Tuple[bytes, bytes, int, bytes],
@@ -106,19 +107,17 @@ class MockTransport(httpcore.AsyncHTTPTransport):
             return b"HTTP/1.1", 200, b"OK", [], content
 
         elif path == b"/redirect_body":
-            _ = b"".join([part async for part in stream])
             code = codes.PERMANENT_REDIRECT
             headers = [(b"location", b"/redirect_body_target")]
             return b"HTTP/1.1", code, b"Permanent Redirect", headers, ByteStream(b"")
 
         elif path == b"/redirect_no_body":
-            _ = b"".join([part async for part in stream])
             code = codes.SEE_OTHER
             headers = [(b"location", b"/redirect_body_target")]
             return b"HTTP/1.1", code, b"See Other", headers, ByteStream(b"")
 
         elif path == b"/redirect_body_target":
-            content = b"".join([part async for part in stream])
+            content = b"".join(stream)
             headers_dict = dict(
                 [(key.decode("ascii"), value.decode("ascii")) for key, value in headers]
             )
@@ -155,9 +154,27 @@ class MockTransport(httpcore.AsyncHTTPTransport):
         return b"HTTP/1.1", 200, b"OK", [], ByteStream(b"Hello, world!")
 
 
+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)
+
+
 @pytest.mark.usefixtures("async_environment")
 async def test_no_redirect():
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     url = "https://example.com/no_redirect"
     response = await client.get(url)
     assert response.status_code == 200
@@ -167,7 +184,7 @@ async def test_no_redirect():
 
 @pytest.mark.usefixtures("async_environment")
 async def test_redirect_301():
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     response = await client.post("https://example.org/redirect_301")
     assert response.status_code == codes.OK
     assert response.url == URL("https://example.org/")
@@ -176,7 +193,7 @@ async def test_redirect_301():
 
 @pytest.mark.usefixtures("async_environment")
 async def test_redirect_302():
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     response = await client.post("https://example.org/redirect_302")
     assert response.status_code == codes.OK
     assert response.url == URL("https://example.org/")
@@ -185,7 +202,7 @@ async def test_redirect_302():
 
 @pytest.mark.usefixtures("async_environment")
 async def test_redirect_303():
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     response = await client.get("https://example.org/redirect_303")
     assert response.status_code == codes.OK
     assert response.url == URL("https://example.org/")
@@ -194,7 +211,7 @@ async def test_redirect_303():
 
 @pytest.mark.usefixtures("async_environment")
 async def test_disallow_redirects():
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     response = await client.post(
         "https://example.org/redirect_303", allow_redirects=False
     )
@@ -212,7 +229,7 @@ async def test_disallow_redirects():
 
 @pytest.mark.usefixtures("async_environment")
 async def test_relative_redirect():
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     response = await client.get("https://example.org/relative_redirect")
     assert response.status_code == codes.OK
     assert response.url == URL("https://example.org/")
@@ -222,7 +239,7 @@ async def test_relative_redirect():
 @pytest.mark.usefixtures("async_environment")
 async def test_malformed_redirect():
     # https://github.com/encode/httpx/issues/771
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     response = await client.get("http://example.org/malformed_redirect")
     assert response.status_code == codes.OK
     assert response.url == URL("https://example.org/")
@@ -231,7 +248,7 @@ async def test_malformed_redirect():
 
 @pytest.mark.usefixtures("async_environment")
 async def test_no_scheme_redirect():
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     response = await client.get("https://example.org/no_scheme_redirect")
     assert response.status_code == codes.OK
     assert response.url == URL("https://example.org/")
@@ -240,7 +257,7 @@ async def test_no_scheme_redirect():
 
 @pytest.mark.usefixtures("async_environment")
 async def test_fragment_redirect():
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     response = await client.get("https://example.org/relative_redirect#fragment")
     assert response.status_code == codes.OK
     assert response.url == URL("https://example.org/#fragment")
@@ -249,7 +266,7 @@ async def test_fragment_redirect():
 
 @pytest.mark.usefixtures("async_environment")
 async def test_multiple_redirects():
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     response = await client.get("https://example.org/multiple_redirects?count=20")
     assert response.status_code == codes.OK
     assert response.url == URL("https://example.org/multiple_redirects")
@@ -265,15 +282,15 @@ async def test_multiple_redirects():
 
 
 @pytest.mark.usefixtures("async_environment")
-async def test_too_many_redirects():
-    client = AsyncClient(transport=MockTransport())
+async def test_async_too_many_redirects():
+    client = AsyncClient(transport=AsyncMockTransport())
     with pytest.raises(TooManyRedirects):
         await client.get("https://example.org/multiple_redirects?count=21")
 
 
 @pytest.mark.usefixtures("async_environment")
-async def test_too_many_redirects_calling_next():
-    client = AsyncClient(transport=MockTransport())
+async def test_async_too_many_redirects_calling_next():
+    client = AsyncClient(transport=AsyncMockTransport())
     url = "https://example.org/multiple_redirects?count=21"
     response = await client.get(url, allow_redirects=False)
     with pytest.raises(TooManyRedirects):
@@ -281,16 +298,31 @@ async def test_too_many_redirects_calling_next():
             response = await response.anext()
 
 
+def test_sync_too_many_redirects():
+    client = Client(transport=SyncMockTransport())
+    with pytest.raises(TooManyRedirects):
+        client.get("https://example.org/multiple_redirects?count=21")
+
+
+def test_sync_too_many_redirects_calling_next():
+    client = Client(transport=SyncMockTransport())
+    url = "https://example.org/multiple_redirects?count=21"
+    response = client.get(url, allow_redirects=False)
+    with pytest.raises(TooManyRedirects):
+        while response.is_redirect:
+            response = response.call_next()
+
+
 @pytest.mark.usefixtures("async_environment")
 async def test_redirect_loop():
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     with pytest.raises(TooManyRedirects):
         await client.get("https://example.org/redirect_loop")
 
 
 @pytest.mark.usefixtures("async_environment")
 async def test_cross_domain_redirect():
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     url = "https://example.com/cross_domain"
     headers = {"Authorization": "abc"}
     response = await client.get(url, headers=headers)
@@ -300,7 +332,7 @@ async def test_cross_domain_redirect():
 
 @pytest.mark.usefixtures("async_environment")
 async def test_same_domain_redirect():
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     url = "https://example.org/cross_domain"
     headers = {"Authorization": "abc"}
     response = await client.get(url, headers=headers)
@@ -313,7 +345,7 @@ async def test_body_redirect():
     """
     A 308 redirect should preserve the request body.
     """
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     url = "https://example.org/redirect_body"
     data = b"Example request body"
     response = await client.post(url, data=data)
@@ -327,7 +359,7 @@ async def test_no_body_redirect():
     """
     A 303 redirect should remove the request body.
     """
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     url = "https://example.org/redirect_no_body"
     data = b"Example request body"
     response = await client.post(url, data=data)
@@ -338,7 +370,7 @@ async def test_no_body_redirect():
 
 @pytest.mark.usefixtures("async_environment")
 async def test_can_stream_if_no_redirect():
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     url = "https://example.org/redirect_301"
     async with client.stream("GET", url, allow_redirects=False) as response:
         assert not response.is_closed
@@ -348,11 +380,11 @@ async def test_can_stream_if_no_redirect():
 
 @pytest.mark.usefixtures("async_environment")
 async def test_cannot_redirect_streaming_body():
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     url = "https://example.org/redirect_body"
 
     async def streaming_body():
-        yield b"Example request body"
+        yield b"Example request body"  # pragma: nocover
 
     with pytest.raises(RequestBodyUnavailable):
         await client.post(url, data=streaming_body())
@@ -360,7 +392,7 @@ async def test_cannot_redirect_streaming_body():
 
 @pytest.mark.usefixtures("async_environment")
 async def test_cross_subdomain_redirect():
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     url = "https://example.com/cross_subdomain"
     response = await client.get(url)
     assert response.url == URL("https://www.example.org/cross_subdomain")
@@ -447,7 +479,7 @@ async def test_redirect_cookie_behavior():
 
 @pytest.mark.usefixtures("async_environment")
 async def test_redirect_custom_scheme():
-    client = AsyncClient(transport=MockTransport())
+    client = AsyncClient(transport=AsyncMockTransport())
     with pytest.raises(InvalidURL) as e:
         await client.post("https://example.org/redirect_custom_scheme")
     assert str(e.value) == 'Scheme "market" not supported.'
index 412fe0e799ba755199f228f5259f233f7e064d92..96a930d1a18a03ab82550bc95b90403d6dacd2b2 100644 (file)
@@ -10,6 +10,6 @@ import trio
 
 async def sleep(seconds: float) -> None:
     if sniffio.current_async_library() == "trio":
-        await trio.sleep(seconds)
+        await trio.sleep(seconds)  # pragma: nocover
     else:
         await asyncio.sleep(seconds)
index 61c06f240a67141ef9d73dfc42a595fe6990ba30..a145ce0fa08abb33256f0ece9a7769556e5d75f3 100644 (file)
@@ -233,7 +233,7 @@ class TestServer(Server):
         }
         await asyncio.wait(tasks)
 
-    async def restart(self) -> None:
+    async def restart(self) -> None:  # pragma: nocover
         # This coroutine may be called from a different thread than the one the
         # server is running on, and from an async environment that's not asyncio.
         # For this reason, we use an event to coordinate with the server
@@ -244,7 +244,7 @@ class TestServer(Server):
         while not self.started:
             await sleep(0.2)
 
-    async def watch_restarts(self):
+    async def watch_restarts(self):  # pragma: nocover
         while True:
             if self.should_exit:
                 return
index f902be521cc28a4f6099304e5ea2b75d973c3f7c..f9a568a31531c1abe06f8fc23e1967524c3db5ca 100644 (file)
@@ -197,6 +197,14 @@ def test_origin_repr():
     assert str(origin) == "Origin(scheme='https' host='example.com' port=8080)"
 
 
+def test_origin_equal():
+    origin1 = Origin("https://example.com")
+    origin2 = Origin("https://example.com")
+    assert origin1 is not origin2
+    assert origin1 == origin2
+    assert len({origin1, origin2}) == 1
+
+
 def test_url_copywith_for_authority():
     copy_with_kwargs = {
         "username": "username",
index d225baf41178c2b4ae6515f26cef1a8bf73bc918..9d68041a88b6f6a4d18c24e2dc267479233628e4 100644 (file)
@@ -1,3 +1,5 @@
+from functools import partial
+
 import pytest
 
 import httpx
@@ -25,8 +27,8 @@ async def echo_body(scope, receive, send):
         await send({"type": "http.response.body", "body": body, "more_body": more_body})
 
 
-async def raise_exc(scope, receive, send):
-    raise ValueError()
+async def raise_exc(scope, receive, send, exc=ValueError):
+    raise exc()
 
 
 async def raise_exc_after_response(scope, receive, send):
@@ -62,6 +64,13 @@ async def test_asgi_exc():
         await client.get("http://www.example.org/")
 
 
+@pytest.mark.asyncio
+async def test_asgi_http_error():
+    client = httpx.AsyncClient(app=partial(raise_exc, exc=httpx.HTTPError))
+    with pytest.raises(httpx.HTTPError):
+        await client.get("http://www.example.org/")
+
+
 @pytest.mark.asyncio
 async def test_asgi_exc_after_response():
     client = httpx.AsyncClient(app=raise_exc_after_response)
index a29412314aafb49d82635185f46ddc0c13b1259c..334bff9a2d8bc20b2ab55a9cab4d709b694f8ce1 100644 (file)
@@ -67,6 +67,11 @@ def test_get_netrc_login():
     assert netrc_info.get_credentials("netrcexample.org") == expected_credentials
 
 
+def test_get_netrc_unknown():
+    netrc_info = NetRCInfo(["tests/.netrc"])
+    assert netrc_info.get_credentials("nonexistant.org") is None
+
+
 @pytest.mark.parametrize(
     "value, expected",
     (
index 1786d9ef28859dc5da71d10700c11e0ffeaa5162..18a272c36f6156ccb171bfc1dea4c576ef446cc1 100644 (file)
@@ -1,4 +1,5 @@
 import sys
+from functools import partial
 
 import pytest
 
@@ -51,7 +52,7 @@ def echo_body_with_response_stream(environ, start_response):
     return output_generator(f=environ["wsgi.input"])
 
 
-def raise_exc(environ, start_response):
+def raise_exc(environ, start_response, exc=ValueError):
     status = "500 Server Error"
     output = b"Nope!"
 
@@ -60,8 +61,8 @@ def raise_exc(environ, start_response):
     ]
 
     try:
-        raise ValueError()
-    except ValueError:
+        raise exc()
+    except exc:
         exc_info = sys.exc_info()
         start_response(status, response_headers, exc_info=exc_info)
 
@@ -95,6 +96,12 @@ def test_wsgi_exc():
         client.get("http://www.example.org/")
 
 
+def test_wsgi_http_error():
+    client = httpx.Client(app=partial(raise_exc, exc=httpx.HTTPError))
+    with pytest.raises(httpx.HTTPError):
+        client.get("http://www.example.org/")
+
+
 def test_wsgi_generator():
     output = [b"", b"", b"Some content", b" and more content"]
     client = httpx.Client(app=application_factory(output))