Files
litellm/tests/test_litellm/proxy/proxy_server/test_routes_misc.py
T
yuneng-jiang 12d29a38a7 tests(proxy_server): surface current behavior in tests (#29309)
* test(proxy/proxy_server): pin forwarding routes (PR2) (#28887)

* test(proxy): pin proxy_server.py forwarding-route behavior

PR2 of the proxy_server.py behavior-pinning project: fills the 12
forwarding-route test files added by the harness PR with happy + error
pins for all 52 LLM-facing routes (models, chat/completions, completions,
embeddings, moderations, audio, assistants, threads, utils, model-info,
model-metrics, queue). Every happy-path test asserts the full response
dict via normalize() so the gate enforces real shape pinning rather
than status codes.

* test(proxy): drop task-plumbing comments from PR2 test files

* test(proxy): tighten PR2 error-path status-code pins

Apply the same review feedback Greptile gave on PR1 (#28856) and PR3
(#28850) to PR2's forwarding-route tests:

- Replace permissive `>= 400` / `in (X, Y)` status assertions with the
  exact 500/405 the handler actually returns, so a regression that
  silently shifts the code now fails the pin.
- Add a body-presence check alongside each tightened status assertion
  to satisfy _pin_check.py's no-status-only rule.

---------

Co-authored-by: Claude <noreply@anthropic.com>

* test(proxy): pin proxy_server.py non-route surface behavior (PR1) (#28856)

* test(proxy): pin proxy_server.py non-route surface behavior (PR1)

Fills the 7 PR1 placeholder files under tests/test_litellm/proxy/proxy_server/
with behavior pins for the non-route surface of proxy_server.py:
lifecycle/init/shutdown, ProxyConfig class methods, DB-overlay config scrubbers,
spend counters, background-health helpers, OpenAPI customization, exception
handlers, and streaming-generator helpers.

233 tests cover 101 pin-list symbols (1+ happy + 1+ error each). New-tests-only
coverage on litellm/proxy/proxy_server.py: 32.80% line / 20.91% branch (PR1
gate: 25% line / 18% branch). Full directory runs in ~22s with -n 4.

Plan: https://www.notion.so/Plan-Pin-proxy_server-py-behavior-2026-05-25-36c43b8acdab81ee845fd5365128a2fc

* test(proxy): address Greptile review comments on test_lifecycle.py

- test_initialize_signature_is_async_with_expected_params: hard-code
  expected_param_count so a signature change actually trips the gate
  (previously both sides of the comparison were len(sig.parameters)).
- test_check_request_disconnection_invalid_when_connected_times_out:
  patch asyncio.sleep so the test no longer spins for ~1.2 s of real
  wall-clock; timeout lowered to 0.05 s.

---------

Co-authored-by: Claude <noreply@anthropic.com>

* test(proxy/proxy_server): pin control-plane routes (PR3) (#28850)

* test(proxy/proxy_server): pin misc routes (PR3, partial)

Adds happy + error tests for the misc control-plane routes:
GET /, /routes, /adaptive_router/state, /get_logo_url,
/get_image, /get_favicon.

Also gitignores .pin_list.txt (used by the pin gate).

* test(proxy/proxy_server): pin login/SSO routes (PR3, partial)

Adds happy + error tests for the 5 login/SSO control-plane routes:
GET /fallback/login, POST /login, POST /v2/login, POST /v3/login,
POST /v3/login/exchange. Mocks authenticate_user and
create_ui_token_object at their imported location.

* test(proxy/proxy_server): pin onboarding routes (PR3, partial)

Adds happy + error tests for the 2 onboarding control-plane routes:
GET /onboarding/get_token, POST /onboarding/claim_token. Wires a
MagicMock async context manager for prisma_client.db.tx() and
signs the onboarding JWT with the patched master_key.

* test(proxy/proxy_server): pin model_cost_map reload routes (PR3, partial)

Adds happy + error tests for the 5 model-cost-map control-plane routes:
POST /reload/model_cost_map, POST|DELETE|GET
/schedule/model_cost_map_reload(/status), GET /model/cost_map/source.
Attaches litellm_config to mock_prisma per-test (the table is not in
the default _PRISMA_TABLES fixture).

* test(proxy/proxy_server): pin anthropic_beta_headers reload routes (PR3, partial)

Adds happy + error tests for the 4 anthropic-beta-headers control-plane
routes: POST /reload/anthropic_beta_headers, POST|DELETE|GET
/schedule/anthropic_beta_headers_reload(/status). Stubs
db.litellm_config (not in default _PRISMA_TABLES) and monkeypatches
reload_beta_headers_config so no network calls fire.

* test(proxy/proxy_server): pin invitation routes (PR3, partial)

Adds happy + error tests for the 4 invitation control-plane routes:
POST /invitation/new, GET /invitation/info, POST /invitation/update,
POST /invitation/delete. Patches _user_has_admin_privileges /
_user_has_admin_view to avoid extensive get_user_object mocking.

* test(proxy/proxy_server): pin config CRUD routes (PR3, partial)

Adds happy + error tests for the 8 config-CRUD control-plane routes:
POST /config/update, POST|GET /config/field/update|info, GET /config/list,
POST /config/field/delete, POST /config/callback/delete,
GET /get/config/callbacks, GET /config/yaml. Attaches litellm_config
to mock_prisma per-test.

* test(proxy/proxy_server): tighten pin assertions per review

- test_routes_misc.py: `b"" in response.content` is trivially true;
  replace with `len(response.content) > 0` so an empty 405 body trips
  the gate.
- test_routes_login_sso.py: `len(response.content) >= 0` is trivially
  true; tighten to `> 0`.
- test_routes_anthropic_beta.py: replace brittle string-literal checks
  on the serialized JSON (`'"interval_hours": 12' in payload`) with
  `json.loads` + dict access so the assertion survives any serializer
  spacing.
- test_routes_config.py: `assert status_code in (404, 500)` was too
  permissive; the handler re-raises HTTPException(404) verbatim, so
  pin 404 strictly.

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-29 23:17:24 -07:00

231 lines
8.2 KiB
Python

"""Pin tests for proxy_server.py misc routes (PR3).
Routes covered:
- GET /
- GET /routes
- GET /adaptive_router/state
- GET /get_logo_url
- GET /get_image
- GET /get_favicon
"""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from .conftest import normalize
# ---------------------------------------------------------------------------
# GET /
# ---------------------------------------------------------------------------
def test_home_returns_200_with_body(client, auth_as):
"""GET / serves either the home string or the Swagger UI fallback —
both return 200 with a non-empty body. This pins the contract: root
always answers and never errors."""
with auth_as():
response = client.get("/")
shape = {
"status": response.status_code,
"has_body": len(response.content) > 0,
"has_content_type": bool(response.headers.get("content-type")),
}
assert shape == {"status": 200, "has_body": True, "has_content_type": True}
def test_home_invalid_method_405(client):
"""GET / handler is GET-only; DELETE returns 405 (error path)."""
response = client.delete("/")
assert response.status_code == 405
assert len(response.content) > 0 and response.headers.get("content-type")
# ---------------------------------------------------------------------------
# GET /routes
# ---------------------------------------------------------------------------
def test_get_routes_returns_routes_list(client, auth_as):
with auth_as():
response = client.get("/routes")
assert response.status_code == 200
body = response.json()
assert isinstance(body, dict)
assert "routes" in body
assert isinstance(body["routes"], list)
assert len(body["routes"]) > 0
sample = body["routes"][0]
shape = {
"has_path": "path" in sample,
"has_methods": "methods" in sample,
"has_endpoint": "endpoint" in sample,
}
assert shape == {
"has_path": True,
"has_methods": True,
"has_endpoint": True,
}
def test_get_routes_invalid_method_405(client):
"""POST against the GET-only /routes endpoint is rejected (error path)."""
response = client.post("/routes")
assert response.status_code == 405
body = response.json() if response.headers.get("content-type", "").startswith(
"application/json"
) else {}
assert isinstance(body, dict)
# ---------------------------------------------------------------------------
# GET /adaptive_router/state
# ---------------------------------------------------------------------------
def test_adaptive_router_state_returns_snapshots(client, auth_as, monkeypatch):
from litellm.proxy import proxy_server as ps
from litellm.proxy._types import LitellmUserRoles
fake_router = MagicMock()
snap = {"router_name": "ar-1", "queue_depth": 0, "posteriors": []}
bandit = MagicMock()
bandit.get_state_snapshot = AsyncMock(return_value=snap)
fake_router.adaptive_routers = {"ar-1": bandit}
monkeypatch.setattr(ps, "llm_router", fake_router)
with auth_as(LitellmUserRoles.PROXY_ADMIN):
response = client.get("/adaptive_router/state")
assert response.status_code == 200
assert normalize(response.json()) == {
"routers": [
{"router_name": "ar-1", "queue_depth": 0, "posteriors": []},
]
}
def test_adaptive_router_state_not_admin_forbidden(client, auth_as):
from litellm.proxy._types import LitellmUserRoles
with auth_as(LitellmUserRoles.INTERNAL_USER):
response = client.get("/adaptive_router/state")
assert response.status_code == 403
assert "error" in response.json().get("detail", {})
def test_adaptive_router_state_not_configured_404(client, auth_as, monkeypatch):
from litellm.proxy import proxy_server as ps
from litellm.proxy._types import LitellmUserRoles
fake_router = MagicMock()
fake_router.adaptive_routers = {}
monkeypatch.setattr(ps, "llm_router", fake_router)
with auth_as(LitellmUserRoles.PROXY_ADMIN):
response = client.get("/adaptive_router/state")
assert response.status_code == 404
assert "adaptive_router" in response.json().get("detail", {}).get("error", "")
# ---------------------------------------------------------------------------
# GET /get_logo_url
# ---------------------------------------------------------------------------
def test_get_logo_url_returns_http_url_when_set(client, monkeypatch):
monkeypatch.setenv("UI_LOGO_PATH", "https://example.invalid/logo.png")
response = client.get("/get_logo_url")
assert response.status_code == 200
assert normalize(response.json()) == {"logo_url": "https://example.invalid/logo.png"}
def test_get_logo_url_blank_when_local_path(client, monkeypatch):
"""Local filesystem paths must NOT be disclosed via this endpoint."""
monkeypatch.setenv("UI_LOGO_PATH", "/var/lib/litellm/internal-secret-logo.png")
response = client.get("/get_logo_url")
assert response.status_code == 200
assert normalize(response.json()) == {"logo_url": ""}
def test_get_logo_url_blank_when_unset(client, monkeypatch):
monkeypatch.delenv("UI_LOGO_PATH", raising=False)
response = client.get("/get_logo_url")
assert response.status_code == 200
assert normalize(response.json()) == {"logo_url": ""}
def test_get_logo_url_invalid_scheme_blank(client, monkeypatch):
"""file:// and other non-HTTP schemes are not disclosed (error/edge path)."""
monkeypatch.setenv("UI_LOGO_PATH", "file:///etc/passwd")
response = client.get("/get_logo_url")
assert response.status_code == 200
assert normalize(response.json()) == {"logo_url": ""}
# ---------------------------------------------------------------------------
# GET /get_image
# ---------------------------------------------------------------------------
def test_get_image_returns_default_logo(client, monkeypatch):
monkeypatch.delenv("UI_LOGO_PATH", raising=False)
response = client.get("/get_image")
assert response.status_code == 200
media_type = response.headers.get("content-type", "").split(";")[0]
shape = {
"status": response.status_code,
"media_type_image": media_type.startswith("image/"),
"has_body": len(response.content) > 0,
}
assert shape == {"status": 200, "media_type_image": True, "has_body": True}
def test_get_image_redirects_remote_url(client, monkeypatch):
"""Remote logo URLs are served via redirect — the proxy never fetches them server-side."""
monkeypatch.setenv("UI_LOGO_PATH", "https://example.invalid/logo.png")
response = client.get("/get_image", follow_redirects=False)
assert response.status_code in (302, 303, 307, 308)
assert response.headers.get("location") == "https://example.invalid/logo.png"
def test_get_image_invalid_local_path_falls_back(client, monkeypatch):
"""Non-existent UI_LOGO_PATH (error path) falls back to default logo, still 200."""
monkeypatch.setenv("UI_LOGO_PATH", "/nonexistent/path/to/logo.png")
response = client.get("/get_image")
assert response.status_code == 200
shape = {
"status": response.status_code,
"media_type_image": response.headers.get("content-type", "").startswith(
"image/"
),
"has_body": len(response.content) > 0,
}
assert shape == {"status": 200, "media_type_image": True, "has_body": True}
# ---------------------------------------------------------------------------
# GET /get_favicon
# ---------------------------------------------------------------------------
def test_get_favicon_returns_file(client):
response = client.get("/get_favicon")
assert response.status_code == 200
shape = {
"status": response.status_code,
"has_body": len(response.content) > 0,
"content_type_set": bool(response.headers.get("content-type")),
}
assert shape == {"status": 200, "has_body": True, "content_type_set": True}
def test_get_favicon_invalid_custom_path_falls_back(client, monkeypatch):
"""Bad UI_FAVICON_PATH (error/edge path) falls back to default — still 200."""
monkeypatch.setenv("UI_FAVICON_PATH", "/nonexistent/favicon.ico")
response = client.get("/get_favicon")
assert response.status_code == 200
assert len(response.content) > 0