Replace httpx and httpx-sse with httpx2 (#2972)
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
This commit is contained in:
parent
1216c53693
commit
2713b53b12
3 changed files with 1029 additions and 948 deletions
|
|
@ -7,9 +7,9 @@ import unittest.mock
|
|||
from typing import Any
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import httpx
|
||||
import httpx2
|
||||
import pytest
|
||||
from httpx import ASGITransport
|
||||
from httpx2 import ASGITransport
|
||||
from pydantic import AnyHttpUrl
|
||||
from starlette.applications import Starlette
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ def app(oauth_provider: MockOAuthProvider):
|
|||
def client(app: Starlette):
|
||||
transport = ASGITransport(app=app)
|
||||
# Use base_url without a path since routes are directly on the app
|
||||
return httpx.AsyncClient(transport=transport, base_url="http://localhost")
|
||||
return httpx2.AsyncClient(transport=transport, base_url="http://localhost")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -65,7 +65,7 @@ def pkce_challenge():
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
async def registered_client(client: httpx.AsyncClient) -> dict[str, Any]:
|
||||
async def registered_client(client: httpx2.AsyncClient) -> dict[str, Any]:
|
||||
"""Create and register a test client."""
|
||||
# Default client metadata
|
||||
client_metadata = {
|
||||
|
|
@ -84,7 +84,7 @@ async def registered_client(client: httpx.AsyncClient) -> dict[str, Any]:
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_registration_error_handling(client: httpx.AsyncClient, oauth_provider: MockOAuthProvider):
|
||||
async def test_registration_error_handling(client: httpx2.AsyncClient, oauth_provider: MockOAuthProvider):
|
||||
# Mock the register_client method to raise a registration error
|
||||
with unittest.mock.patch.object(
|
||||
oauth_provider,
|
||||
|
|
@ -118,7 +118,7 @@ async def test_registration_error_handling(client: httpx.AsyncClient, oauth_prov
|
|||
|
||||
@pytest.mark.anyio
|
||||
async def test_authorize_error_handling(
|
||||
client: httpx.AsyncClient,
|
||||
client: httpx2.AsyncClient,
|
||||
oauth_provider: MockOAuthProvider,
|
||||
registered_client: dict[str, Any],
|
||||
pkce_challenge: dict[str, str],
|
||||
|
|
@ -159,7 +159,7 @@ async def test_authorize_error_handling(
|
|||
|
||||
@pytest.mark.anyio
|
||||
async def test_token_error_handling_auth_code(
|
||||
client: httpx.AsyncClient,
|
||||
client: httpx2.AsyncClient,
|
||||
oauth_provider: MockOAuthProvider,
|
||||
registered_client: dict[str, Any],
|
||||
pkce_challenge: dict[str, str],
|
||||
|
|
@ -218,7 +218,7 @@ async def test_token_error_handling_auth_code(
|
|||
|
||||
@pytest.mark.anyio
|
||||
async def test_token_error_handling_refresh_token(
|
||||
client: httpx.AsyncClient,
|
||||
client: httpx2.AsyncClient,
|
||||
oauth_provider: MockOAuthProvider,
|
||||
registered_client: dict[str, Any],
|
||||
pkce_challenge: dict[str, str],
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
import secrets
|
||||
import time
|
||||
|
||||
import httpx
|
||||
import httpx2
|
||||
import pytest
|
||||
from httpx import ASGITransport
|
||||
from httpx2 import ASGITransport
|
||||
from pydantic import AnyHttpUrl
|
||||
from starlette.applications import Starlette
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ def app(provider: IdentityAssertionProvider) -> Starlette:
|
|||
@pytest.fixture
|
||||
async def client(app: Starlette):
|
||||
transport = ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="https://auth.example.com") as http:
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="https://auth.example.com") as http:
|
||||
yield http
|
||||
|
||||
|
||||
|
|
@ -160,7 +160,7 @@ def test_build_metadata_advertises_id_jag_profile_when_enabled():
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_metadata_endpoint_lists_id_jag_profile(client: httpx.AsyncClient):
|
||||
async def test_metadata_endpoint_lists_id_jag_profile(client: httpx2.AsyncClient):
|
||||
response = await client.get("/.well-known/oauth-authorization-server")
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
|
|
@ -169,7 +169,7 @@ async def test_metadata_endpoint_lists_id_jag_profile(client: httpx.AsyncClient)
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_identity_assertion_success(client: httpx.AsyncClient, provider: IdentityAssertionProvider):
|
||||
async def test_identity_assertion_success(client: httpx2.AsyncClient, provider: IdentityAssertionProvider):
|
||||
response = await client.post("/token", data=assertion_form(scope="mcp", resource="https://mcp.example.com/mcp"))
|
||||
|
||||
assert response.status_code == 200, response.content
|
||||
|
|
@ -189,7 +189,7 @@ async def test_identity_assertion_success(client: httpx.AsyncClient, provider: I
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_identity_assertion_invalid_assertion(client: httpx.AsyncClient):
|
||||
async def test_identity_assertion_invalid_assertion(client: httpx2.AsyncClient):
|
||||
response = await client.post("/token", data=assertion_form(assertion="forged"))
|
||||
|
||||
assert response.status_code == 400
|
||||
|
|
@ -207,7 +207,7 @@ async def test_identity_assertion_rejected_when_disabled(provider: IdentityAsser
|
|||
)
|
||||
app = Starlette(routes=routes)
|
||||
transport = ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="https://auth.example.com") as http:
|
||||
async with httpx2.AsyncClient(transport=transport, base_url="https://auth.example.com") as http:
|
||||
response = await http.post("/token", data=assertion_form())
|
||||
|
||||
assert response.status_code == 400
|
||||
|
|
@ -216,7 +216,9 @@ async def test_identity_assertion_rejected_when_disabled(provider: IdentityAsser
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_identity_assertion_rejects_public_client(client: httpx.AsyncClient, provider: IdentityAssertionProvider):
|
||||
async def test_identity_assertion_rejects_public_client(
|
||||
client: httpx2.AsyncClient, provider: IdentityAssertionProvider
|
||||
):
|
||||
"""A public (auth method 'none') client cannot use the grant, even if it presents a valid assertion."""
|
||||
provider.clients["public-client"] = OAuthClientInformationFull(
|
||||
client_id="public-client",
|
||||
|
|
@ -238,7 +240,7 @@ async def test_identity_assertion_rejects_public_client(client: httpx.AsyncClien
|
|||
|
||||
@pytest.mark.anyio
|
||||
async def test_identity_assertion_rejects_secretless_confidential_client(
|
||||
client: httpx.AsyncClient, provider: IdentityAssertionProvider
|
||||
client: httpx2.AsyncClient, provider: IdentityAssertionProvider
|
||||
):
|
||||
"""A client registered with a secret-based method but no stored secret fails authentication.
|
||||
|
||||
|
|
@ -267,7 +269,7 @@ async def test_identity_assertion_rejects_secretless_confidential_client(
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_malformed_request_missing_assertion_is_invalid_request(client: httpx.AsyncClient):
|
||||
async def test_malformed_request_missing_assertion_is_invalid_request(client: httpx2.AsyncClient):
|
||||
"""A jwt-bearer request without the required `assertion` fails validation with invalid_request."""
|
||||
response = await client.post(
|
||||
"/token",
|
||||
|
|
@ -284,7 +286,7 @@ async def test_malformed_request_missing_assertion_is_invalid_request(client: ht
|
|||
|
||||
@pytest.mark.anyio
|
||||
async def test_client_without_the_grant_registered_is_rejected(
|
||||
client: httpx.AsyncClient, provider: IdentityAssertionProvider
|
||||
client: httpx2.AsyncClient, provider: IdentityAssertionProvider
|
||||
):
|
||||
"""A confidential client whose registration omits the jwt-bearer grant is refused the grant."""
|
||||
provider.clients["no-grant-client"] = OAuthClientInformationFull(
|
||||
|
|
@ -313,7 +315,7 @@ async def test_client_without_the_grant_registered_is_rejected(
|
|||
|
||||
@pytest.mark.anyio
|
||||
async def test_dcr_refuses_to_register_the_jwt_bearer_grant(
|
||||
client: httpx.AsyncClient, provider: IdentityAssertionProvider
|
||||
client: httpx2.AsyncClient, provider: IdentityAssertionProvider
|
||||
):
|
||||
"""Dynamic client registration rejects the jwt-bearer grant; the ID-JAG flow needs pre-registration."""
|
||||
response = await client.post(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
import httpx2
|
||||
import pytest
|
||||
from inline_snapshot import snapshot
|
||||
from pydantic import AnyHttpUrl
|
||||
|
|
@ -31,12 +31,14 @@ def test_app():
|
|||
@pytest.fixture
|
||||
async def test_client(test_app: Starlette):
|
||||
"""Fixture to create an HTTP client for the protected resource app."""
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=test_app), base_url="https://mcptest.com") as client:
|
||||
async with httpx2.AsyncClient(
|
||||
transport=httpx2.ASGITransport(app=test_app), base_url="https://mcptest.com"
|
||||
) as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_metadata_endpoint_with_path(test_client: httpx.AsyncClient):
|
||||
async def test_metadata_endpoint_with_path(test_client: httpx2.AsyncClient):
|
||||
"""Test the OAuth 2.0 Protected Resource metadata endpoint for path-based resource."""
|
||||
|
||||
# For resource with path "/resource", metadata should be accessible at the path-aware location
|
||||
|
|
@ -54,7 +56,7 @@ async def test_metadata_endpoint_with_path(test_client: httpx.AsyncClient):
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_metadata_endpoint_root_path_returns_404(test_client: httpx.AsyncClient):
|
||||
async def test_metadata_endpoint_root_path_returns_404(test_client: httpx2.AsyncClient):
|
||||
"""Test that root path returns 404 for path-based resource."""
|
||||
|
||||
# Root path should return 404 for path-based resources
|
||||
|
|
@ -81,14 +83,14 @@ def root_resource_app():
|
|||
@pytest.fixture
|
||||
async def root_resource_client(root_resource_app: Starlette):
|
||||
"""Fixture to create an HTTP client for the root resource app."""
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=root_resource_app), base_url="https://mcptest.com"
|
||||
async with httpx2.AsyncClient(
|
||||
transport=httpx2.ASGITransport(app=root_resource_app), base_url="https://mcptest.com"
|
||||
) as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_metadata_endpoint_without_path(root_resource_client: httpx.AsyncClient):
|
||||
async def test_metadata_endpoint_without_path(root_resource_client: httpx2.AsyncClient):
|
||||
"""Test metadata endpoint for root-level resource."""
|
||||
|
||||
# For root resource, metadata should be at standard location
|
||||
|
|
|
|||
Loading…
Reference in a new issue