Skip to content

Commit 0ee7f76

Browse files
committed
test: verify eager refresh is skipped when oauth_metadata is None
Regression test for #3240. When oauth_metadata is None (metadata not yet discovered), the eager refresh at the top of async_auth_flow must be skipped to avoid hitting the wrong token endpoint (urljoin strips non-root paths). The test sets up expired tokens + refresh_token but no oauth_metadata, then verifies the first yield is the original request (not a refresh request) and the tokens are preserved for the 401→discovery→refresh path.
1 parent ebff435 commit 0ee7f76

1 file changed

Lines changed: 47 additions & 0 deletions

File tree

tests/client/test_auth.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,6 +1290,53 @@ async def test_token_exchange_accepts_201_status(
12901290
request=discovery_request,
12911291
)
12921292

1293+
1294+
@pytest.mark.anyio
1295+
async def test_auth_flow_skips_eager_refresh_when_metadata_missing(
1296+
self, oauth_provider: OAuthClientProvider, mock_storage: MockTokenStorage
1297+
):
1298+
"""When oauth_metadata is None the eager refresh must be skipped.
1299+
1300+
Without metadata the token endpoint is unknown; the fallback
1301+
urljoin(base_url, '/token') strips the path when the AS lives under a
1302+
non-root path (e.g. /oauth2/api/v1/token). The fix guards the eager
1303+
refresh on ``oauth_metadata is not None`` so the stale-token request
1304+
proceeds, gets a 401, and runs full metadata discovery instead.
1305+
"""
1306+
# Set up expired tokens with a refresh token but NO oauth_metadata.
1307+
expired_tokens = OAuthToken(
1308+
access_token="expired_access_token",
1309+
token_type="Bearer",
1310+
expires_in=0,
1311+
refresh_token="test_refresh_token",
1312+
scope="read write",
1313+
)
1314+
await mock_storage.set_tokens(expired_tokens)
1315+
oauth_provider.context.current_tokens = expired_tokens
1316+
oauth_provider.context.token_expiry_time = time.time() - 100 # Expired
1317+
oauth_provider._initialized = True
1318+
oauth_provider.context.client_info = OAuthClientInformationFull(
1319+
client_id="test_client",
1320+
redirect_uris=[AnyUrl("http://localhost:3030/callback")],
1321+
)
1322+
# oauth_metadata is None (default) — this is the key condition.
1323+
1324+
test_request = httpx2.Request("GET", "https://api.example.com/v1/mcp")
1325+
auth_flow = oauth_provider.async_auth_flow(test_request)
1326+
1327+
# The first yield should be the original request WITHOUT an auth header,
1328+
# NOT a refresh request to the wrong endpoint.
1329+
request = await auth_flow.__anext__()
1330+
assert "Authorization" not in request.headers
1331+
assert str(request.url) == "https://api.example.com/v1/mcp"
1332+
assert request.method == "GET"
1333+
1334+
# The token was not consumed by a failed refresh.
1335+
assert oauth_provider.context.current_tokens is not None
1336+
assert oauth_provider.context.current_tokens.refresh_token == "test_refresh_token"
1337+
1338+
# Close the generator to avoid warnings.
1339+
await auth_flow.aclose()
12931340
# Next request should be to discover OAuth metadata
12941341
oauth_metadata_request = await auth_flow.asend(discovery_response)
12951342
assert oauth_metadata_request.method == "GET"

0 commit comments

Comments
 (0)