refactor(e2e): unroll the auth-header matrix into explicitly named per-header tests

This commit is contained in:
Tin Chi Lo 2026-07-14 20:21:26 -07:00
parent d32974c282
commit b1442d0398
2 changed files with 69 additions and 58 deletions

View file

@ -10,8 +10,7 @@ Every protocol method takes the request headers as a plain dict, built inside
the test body, so the exact wire format is visible where it is asserted. The
gateway accepts the LiteLLM virtual key as either
`x-litellm-api-key: Bearer sk-...` or `Authorization: Bearer sk-...` (both
Bearer-prefixed on the MCP routes, matching the docs); `McpHeaderName` names
those two documented header styles for the tests' parametrized matrices.
Bearer-prefixed on the MCP routes, matching the docs).
"""
from __future__ import annotations
@ -19,7 +18,7 @@ from __future__ import annotations
import asyncio
import time
from dataclasses import dataclass
from typing import Literal, Mapping, cast
from typing import Mapping, cast
import httpx
import pytest
@ -33,8 +32,6 @@ from e2e_gateway import Gateway, build_gateway
from e2e_http import NoBody, unwrap
from models import McpServerCreateBody, McpServerInfo
McpHeaderName = Literal["x-litellm-api-key", "Authorization"]
ToolArguments = Mapping[str, str | float]

View file

@ -8,21 +8,21 @@ all against the deterministic mcp-stub compose service (tests/e2e/mcp/stub/).
Each test body spells out every step a human QA run would take, in order:
create the server over the management API, read the record back, mint a
virtual key over /key/generate, build the exact wire header
(`<header>: Bearer sk-...`, the documented contract on the MCP routes; a bare
key in `x-litellm-api-key` is accepted on LLM routes but 401s here), then
drive the real MCP protocol through the gateway the way a production MCP host
does (initialize, tools/list, tools/call over streamable HTTP at
virtual key over /key/generate, build the exact wire header, then drive the
real MCP protocol through the gateway the way a production MCP host does
(initialize, tools/list, tools/call over streamable HTTP at
{PROXY}/{alias}/mcp) and assert the enforced behavior. Teardown is the only
thing delegated (resources.defer), because it must run even when an
assertion fails.
The two header styles are one behavior matrix, not two behaviors: the same
test body runs once per header via parametrize, so the specs cannot drift
apart, and each parametrization claims its own registry cells through
param-level `covers` marks. The unknown-key rejection is its own test (also
run per header) and settles the server with a real key first, so its 401 can
only be the gateway's ingress auth refusing the key, never record propagation.
The two documented header styles are deliberately separate, explicitly named
tests rather than one parametrized body, so the literal header each one sends
(`x-litellm-api-key: Bearer sk-...` / `Authorization: Bearer sk-...`) is
visible in its own body. Both are Bearer-prefixed on the MCP routes, matching
the docs; a bare key in `x-litellm-api-key` is accepted on LLM routes but
401s here. The unknown-key rejection tests settle the server with a real key
first, so their 401 can only be the gateway's ingress auth refusing the key,
never record propagation.
"""
from __future__ import annotations
@ -31,54 +31,23 @@ import pytest
from e2e_config import MCP_STUB_URL, unique_marker
from lifecycle import ResourceManager
from mcp_client import McpClient, McpDenied, McpHeaderName
from mcp_client import McpClient, McpDenied
from models import KeyGenerateBody, McpServerCreateBody
pytestmark = pytest.mark.e2e
STUB_TOOLS = ("echo", "slow_echo", "stats")
AUTH_HEADER_MATRIX = (
pytest.param(
"x-litellm-api-key",
id="x-litellm-api-key",
marks=(
pytest.mark.covers("mcp.list_tools.api_key.succeeds"),
pytest.mark.covers("mcp.call_tool.api_key.succeeds"),
),
),
pytest.param(
"Authorization",
id="authorization-bearer",
marks=(
pytest.mark.covers("mcp.list_tools.bearer.succeeds"),
pytest.mark.covers("mcp.call_tool.bearer.succeeds"),
),
),
)
REJECTION_MATRIX = (
pytest.param(
"x-litellm-api-key",
id="x-litellm-api-key",
marks=pytest.mark.covers("mcp.list_tools.api_key.rejects_unknown_key"),
),
pytest.param(
"Authorization",
id="authorization-bearer",
marks=pytest.mark.covers("mcp.list_tools.bearer.rejects_unknown_key"),
),
)
class TestMcpToolAccess:
"""Any virtual key reaches an allow_all_keys server through either
documented auth header; a key the proxy does not recognize is turned away
at the door."""
@pytest.mark.parametrize("header_name", AUTH_HEADER_MATRIX)
def test_list_and_call_tools(
self, header_name: McpHeaderName, client: McpClient, resources: ResourceManager
@pytest.mark.covers("mcp.list_tools.api_key.succeeds")
@pytest.mark.covers("mcp.call_tool.api_key.succeeds")
def test_list_and_call_tools_with_x_litellm_api_key_header(
self, client: McpClient, resources: ResourceManager
) -> None:
alias = f"e2emcp{unique_marker()}"
created = client.create_server(McpServerCreateBody(alias=alias, url=MCP_STUB_URL, allow_all_keys=True))
@ -92,7 +61,7 @@ class TestMcpToolAccess:
key = client.gateway.generate_key(KeyGenerateBody())
resources.defer(lambda: client.gateway.delete_key(key))
headers = {header_name: f"Bearer {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 STUB_TOOLS))
assert names == expected, f"gateway listed {names}, expected exactly {expected}"
@ -102,9 +71,36 @@ class TestMcpToolAccess:
assert result.is_error is False, f"echo call errored: {result.text[:300]}"
assert result.text == payload
@pytest.mark.parametrize("header_name", REJECTION_MATRIX)
def test_unrecognized_key_is_turned_away(
self, header_name: McpHeaderName, client: McpClient, resources: ResourceManager
@pytest.mark.covers("mcp.list_tools.bearer.succeeds")
@pytest.mark.covers("mcp.call_tool.bearer.succeeds")
def test_list_and_call_tools_with_authorization_bearer_header(
self, client: McpClient, resources: ResourceManager
) -> None:
alias = f"e2emcp{unique_marker()}"
created = client.create_server(McpServerCreateBody(alias=alias, url=MCP_STUB_URL, allow_all_keys=True))
resources.defer(lambda: client.delete_server(created.server_id))
stored = client.server_info(created.server_id)
assert stored.alias == alias
assert stored.url == MCP_STUB_URL
assert stored.allow_all_keys is True
key = client.gateway.generate_key(KeyGenerateBody())
resources.defer(lambda: client.gateway.delete_key(key))
headers = {"Authorization": f"Bearer {key}"}
names = client.poll_tool_names(alias, headers)
expected = tuple(sorted(f"{alias}-{tool}" for tool in STUB_TOOLS))
assert names == expected, f"gateway 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 call errored: {result.text[:300]}"
assert result.text == payload
@pytest.mark.covers("mcp.list_tools.api_key.rejects_unknown_key")
def test_unrecognized_key_in_x_litellm_api_key_header_is_turned_away(
self, client: McpClient, resources: ResourceManager
) -> None:
"""Settle the server with a real key first, then present an unknown
key over the same header: the refusal must be an explicit 401 from
@ -116,8 +112,26 @@ class TestMcpToolAccess:
key = client.gateway.generate_key(KeyGenerateBody())
resources.defer(lambda: client.gateway.delete_key(key))
_ = client.poll_tool_names(alias, {header_name: f"Bearer {key}"})
_ = client.poll_tool_names(alias, {"x-litellm-api-key": f"Bearer {key}"})
denied = client.list_tools_once(alias, {header_name: "Bearer sk-not-a-real-key"})
denied = client.list_tools_once(alias, {"x-litellm-api-key": "Bearer sk-not-a-real-key"})
assert isinstance(denied, McpDenied), f"unrecognized key was served tools: {denied}"
assert denied.status_code == 401, f"expected 401 for an unrecognized key, got {denied}"
@pytest.mark.covers("mcp.list_tools.bearer.rejects_unknown_key")
def test_unrecognized_key_in_authorization_bearer_header_is_turned_away(
self, client: McpClient, resources: ResourceManager
) -> None:
"""The Authorization form of the same rejection: an unknown key sent
as `Authorization: Bearer ...` must be refused identically."""
alias = f"e2emcp{unique_marker()}"
created = client.create_server(McpServerCreateBody(alias=alias, url=MCP_STUB_URL, allow_all_keys=True))
resources.defer(lambda: client.delete_server(created.server_id))
key = client.gateway.generate_key(KeyGenerateBody())
resources.defer(lambda: client.gateway.delete_key(key))
_ = client.poll_tool_names(alias, {"Authorization": f"Bearer {key}"})
denied = client.list_tools_once(alias, {"Authorization": "Bearer sk-not-a-real-key"})
assert isinstance(denied, McpDenied), f"unrecognized key was served tools: {denied}"
assert denied.status_code == 401, f"expected 401 for an unrecognized key, got {denied}"