mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
refactor(e2e): unroll the auth-header matrix into explicitly named per-header tests
This commit is contained in:
parent
d32974c282
commit
b1442d0398
2 changed files with 69 additions and 58 deletions
|
|
@ -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]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue