refactor(e2e): drop the client_credentials arm; oauth coverage stays on the authorization_code flow

This commit is contained in:
Tin Chi Lo 2026-07-16 13:00:49 -07:00
parent 59fa990c86
commit c84cb8b1e3

View file

@ -1,17 +1,17 @@
"""Live e2e: the gateway attaches the server's stored upstream credentials.
"""Live e2e: the gateway attaches the server's stored upstream credential.
Covers mcp.call_tool.api_key.injects_upstream_credential (a static shared-key
credential is injected as X-API-Key) and mcp.list_tools.oauth.succeeds +
mcp.call_tool.oauth.succeeds (OAuth2 client_credentials: the gateway exchanges
the stored client id/secret at the token endpoint and sends the minted access
token upstream), against the guarded mcp-stub mounts (tests/e2e/mcp/stub/).
Covers mcp.call_tool.api_key.injects_upstream_credential: a static shared-key
credential stored on the server is injected as X-API-Key on every egress
request, against the X-API-Key-guarded mcp-stub mount (tests/e2e/mcp/stub/).
OAuth upstream auth is covered by the authorization_code flow in
test_mcp_oauth_interactive_e2e.py on the base suite.
Both tests follow the suite lifecycle: register the server with credentials
The test follows the suite lifecycle: register the server with credentials
over the management API and defer its deletion, assert the recorded state
round-trips with the secret redacted (GET /v1/mcp/server/{id} echoes the auth
config but nulls `credentials`), then drive initialize + tools/list +
tools/call through the gateway and assert the enforced behavior. The guarded
stub mounts 401 any request that does not carry exactly the expected
stub mount 401s any request that does not carry exactly the expected
credential, so a served call is itself proof of injection; that is the
fail-before-fix evidence built into the design, since a gateway that stops
attaching the credential (or attaches the wrong one) cannot list a single
@ -25,17 +25,7 @@ from __future__ import annotations
import pytest
from e2e_config import (
MCP_STUB_APIKEY_URL,
MCP_STUB_OAUTH_ACCESS_TOKEN,
MCP_STUB_OAUTH_CLIENT_ID,
MCP_STUB_OAUTH_CLIENT_SECRET,
MCP_STUB_OAUTH_SCOPE,
MCP_STUB_OAUTH_URL,
MCP_STUB_TOKEN_URL,
MCP_STUB_UPSTREAM_API_KEY,
unique_marker,
)
from e2e_config import MCP_STUB_APIKEY_URL, MCP_STUB_UPSTREAM_API_KEY, unique_marker
from lifecycle import ResourceManager
from mcp_client import McpClient
from models import KeyGenerateBody, McpServerCreateBody, McpServerCredentials
@ -88,60 +78,3 @@ class TestMcpUpstreamSharedKeyInjection:
assert upstream_headers.get("x-api-key") == MCP_STUB_UPSTREAM_API_KEY
leaked = sorted(name for name, value in upstream_headers.items() if key in value)
assert leaked == [], f"caller's virtual key crossed the gateway boundary in header(s) {leaked}"
class TestMcpOauth2ClientCredentials:
"""A server stored with `auth_type: oauth2` in the client_credentials (M2M)
flow works end to end: the gateway exchanges the stored client id/secret
(with the configured scope) at the token endpoint and presents the minted
access token upstream; the caller's own credential stays at the gateway."""
@pytest.mark.covers("mcp.list_tools.oauth.succeeds")
@pytest.mark.covers("mcp.call_tool.oauth.succeeds")
def test_gateway_exchanges_client_credentials_and_sends_minted_token(
self, client: McpClient, resources: ResourceManager
) -> None:
alias = f"e2emcpoauth{unique_marker()}"
created = client.create_server(
McpServerCreateBody(
alias=alias,
url=MCP_STUB_OAUTH_URL,
allow_all_keys=True,
auth_type="oauth2",
oauth2_flow="client_credentials",
token_url=MCP_STUB_TOKEN_URL,
credentials=McpServerCredentials(
client_id=MCP_STUB_OAUTH_CLIENT_ID,
client_secret=MCP_STUB_OAUTH_CLIENT_SECRET,
scopes=[MCP_STUB_OAUTH_SCOPE],
),
)
)
resources.defer(lambda: client.delete_server(created.server_id))
stored = client.server_info(created.server_id)
assert stored.auth_type == "oauth2"
assert stored.oauth2_flow == "client_credentials"
assert stored.token_url == MCP_STUB_TOKEN_URL
assert stored.credentials is None, f"client secret must be redacted on read-back, got {stored.credentials}"
key = client.gateway.generate_key(KeyGenerateBody())
resources.defer(lambda: client.gateway.delete_key(key))
headers = {"x-litellm-api-key": f"Bearer {key}"}
names = client.poll_tool_names(alias, headers)
expected = tuple(sorted(f"{alias}-{tool}" for tool in GUARDED_STUB_TOOLS))
assert names == expected, f"oauth upstream listed {names}, expected exactly {expected}"
payload = f"e2e-{unique_marker()}"
result = client.call_tool(alias, headers, f"{alias}-echo", {"text": payload})
assert result.is_error is False, f"echo through the oauth upstream errored: {result.text[:300]}"
assert result.text == payload
upstream_headers = client.stub_recorded_headers(alias, headers, f"{alias}-recorded_headers")
assert upstream_headers.get("authorization") == f"Bearer {MCP_STUB_OAUTH_ACCESS_TOKEN}", (
"upstream must receive exactly the token the stub IdP mints for the stored client credentials, "
f"got {upstream_headers.get('authorization')!r}"
)
leaked = sorted(name for name, value in upstream_headers.items() if key in value)
assert leaked == [], f"caller's virtual key crossed the gateway boundary in header(s) {leaked}"