From d1c2674e6cbc280fee76a427e9545769f331c534 Mon Sep 17 00:00:00 2001 From: Tin Date: Thu, 12 Nov 2026 16:39:12 -0500 Subject: [PATCH] test: tighten shared OAuth and retired Lens startup coverage --- .../test_user_api_key_auth_request_flow.py | 66 ++++++++++--------- .../unit/proxy/proxy_server/test_lifecycle.py | 58 ++++++++++++++++ 2 files changed, 93 insertions(+), 31 deletions(-) diff --git a/tests/unit/proxy/auth/test_user_api_key_auth_request_flow.py b/tests/unit/proxy/auth/test_user_api_key_auth_request_flow.py index c94f5e3ae3b..84a39e23082 100644 --- a/tests/unit/proxy/auth/test_user_api_key_auth_request_flow.py +++ b/tests/unit/proxy/auth/test_user_api_key_auth_request_flow.py @@ -3126,27 +3126,28 @@ async def test_jwt_auto_register_forwards_bound_agent_id(): @pytest.mark.parametrize("route", ("/team/list", "/model/info")) @pytest.mark.parametrize("jwt_override", (False, True)) @pytest.mark.parametrize("active", (True, False)) -async def test_oauth2_info_routes_validate_introspection_request( - route: str, jwt_override: bool, active: bool -) -> None: +async def test_oauth2_info_routes_validate_introspection_request(route: str, jwt_override: bool, active: bool) -> None: import httpx import jwt + from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler + token: Final = ( jwt.encode({"iss": "oauth-fixture", "sub": "fixture-client"}, "fixture-signing-key-for-dispatch-only") if jwt_override else "fixture-opaque-token+with/slash=" ) + cache: Final = UserApiKeyCache() handler: Final = JWTHandler() handler.update_environment( prisma_client=None, - user_api_key_cache=DualCache(), + user_api_key_cache=cache, litellm_jwtauth=LiteLLM_JWTAuth(routing_overrides=[JWTRoutingOverride(iss="oauth-fixture", path="oauth2")]), ) settings: Final = {"enable_oauth2_auth": not jwt_override, "enable_jwt_auth": True, "enforce_user_param": True} request: Final = Request({"type": "http", "method": "GET", "path": route, "headers": [], "query_string": b""}) - async def token_info(outbound: httpx.Request, **kwargs: object) -> httpx.Response: + async def token_info(outbound: httpx.Request) -> httpx.Response: assert str(outbound.url) == "https://oauth.fixture.test/introspect" assert outbound.method == "POST" assert outbound.headers["content-type"] == "application/x-www-form-urlencoded" @@ -3157,32 +3158,35 @@ async def test_oauth2_info_routes_validate_introspection_request( json={"active": active, "sub": "oauth-user", "role": "proxy_admin"}, ) - with ( - patch("litellm.proxy.proxy_server.general_settings", settings), - patch("litellm.proxy.proxy_server.jwt_handler", handler), - patch("litellm.proxy.proxy_server.premium_user", True), - patch("litellm.proxy.proxy_server.master_key", "sk-fixture-master"), - patch("litellm.proxy.proxy_server.prisma_client", None), - patch("litellm.proxy.proxy_server.user_api_key_cache", DualCache()), - patch.dict( - os.environ, - { - "OAUTH_TOKEN_INFO_ENDPOINT": "https://oauth.fixture.test/introspect", - "OAUTH_CLIENT_ID": "fixture-client", - "OAUTH_CLIENT_SECRET": "fixture-client-secret", - "OAUTH_USER_ID_FIELD_NAME": "sub", - "OAUTH_USER_ROLE_FIELD_NAME": "role", - }, - ), - patch("httpx.AsyncClient.send", side_effect=token_info) as network, - ): - if active: - identity: Final = await user_api_key_auth(request=request, api_key=f"Bearer {token}") - assert (identity.user_id, identity.user_role) == ("oauth-user", LitellmUserRoles.PROXY_ADMIN) - else: - with pytest.raises(ProxyException, match="Token is not active"): - await user_api_key_auth(request=request, api_key=f"Bearer {token}") - assert network.call_count == 1 + network: Final = AsyncMock(wraps=token_info) + client: Final = AsyncHTTPHandler(transport=httpx.MockTransport(network)) + async with client.client: + with ( + patch("litellm.proxy.proxy_server.general_settings", settings), + patch("litellm.proxy.proxy_server.jwt_handler", handler), + patch("litellm.proxy.proxy_server.premium_user", True), + patch("litellm.proxy.proxy_server.master_key", "sk-fixture-master"), + patch("litellm.proxy.proxy_server.prisma_client", None), + patch("litellm.proxy.proxy_server.user_api_key_cache", cache), + patch.dict( + os.environ, + { + "OAUTH_TOKEN_INFO_ENDPOINT": "https://oauth.fixture.test/introspect", + "OAUTH_CLIENT_ID": "fixture-client", + "OAUTH_CLIENT_SECRET": "fixture-client-secret", + "OAUTH_USER_ID_FIELD_NAME": "sub", + "OAUTH_USER_ROLE_FIELD_NAME": "role", + }, + ), + patch("litellm.proxy.auth.oauth2_check.get_async_httpx_client", return_value=client), + ): + if active: + identity: Final = await user_api_key_auth(request=request, api_key=f"Bearer {token}") + assert (identity.user_id, identity.user_role) == ("oauth-user", LitellmUserRoles.PROXY_ADMIN) + else: + with pytest.raises(ProxyException, match="Token is not active"): + await user_api_key_auth(request=request, api_key=f"Bearer {token}") + assert network.await_count == 1 class TestJWTOAuth2Coexistence: diff --git a/tests/unit/proxy/proxy_server/test_lifecycle.py b/tests/unit/proxy/proxy_server/test_lifecycle.py index 7f99d12c868..e363d7a3ced 100644 --- a/tests/unit/proxy/proxy_server/test_lifecycle.py +++ b/tests/unit/proxy/proxy_server/test_lifecycle.py @@ -26,11 +26,13 @@ import subprocess from collections.abc import AsyncIterator, Awaitable, Callable from contextlib import asynccontextmanager from datetime import datetime, timedelta, timezone +from pathlib import Path from typing import Final from unittest.mock import AsyncMock, MagicMock, patch import pytest from apscheduler.schedulers.asyncio import AsyncIOScheduler +from fastapi import FastAPI from pydantic import BaseModel from typing_extensions import TypedDict @@ -53,6 +55,7 @@ from litellm.proxy.proxy_server import ( save_worker_config, ) from litellm.proxy.utils import PrismaClient +from tests._master_key import MASTER_KEY from .conftest import normalize @@ -1023,6 +1026,61 @@ async def test_proxy_startup_event_honors_disable_model_info_refresh( ) +@pytest.mark.asyncio +@pytest.mark.parametrize( + "environment_name", + (None, "LITELLM_LENS_URL", "LITELLM_LENS_PUBLIC_URL", "LITELLM_LENS_SERVICE_TOKEN", "LENS_GATEWAY_SECRET"), + ids=("yaml", "url", "public-url", "service-token", "gateway-secret"), +) +async def test_proxy_startup_warns_for_retired_lens_settings_without_disclosing_values( + environment_name: str | None, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + sentinel: Final = "https://private-lens-config-sentinel.invalid" + secret: Final = "private-lens-secret-sentinel" + tracing_yaml: Final = ( + f" tracing:\n store:\n type: lens\n url: {sentinel}\n token: {secret}\n" + if environment_name is None + else "" + ) + config_file: Final = tmp_path / "config.yaml" + config_file.write_text( + "model_list: []\ngeneral_settings:\n disable_model_info_refresh: true\n" + tracing_yaml, + encoding="utf-8", + ) + environment: Final = ( + {environment_name: sentinel if environment_name.endswith("URL") else secret} + if environment_name is not None + else {} + ) + monkeypatch.setattr(ps, "proxy_config", ps.ProxyConfig()) + with ( + patch.dict( + os.environ, + { + "CONFIG_FILE_PATH": str(config_file), + "LITELLM_MASTER_KEY": MASTER_KEY, + "LITELLM_LOCAL_MODEL_COST_MAP": "True", + "PYTHON_DOTENV_DISABLED": "1", + **environment, + }, + clear=True, + ), + caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"), + ): + async with proxy_startup_event(FastAPI()): + assert ( + "LiteLLM Proxy", + logging.WARNING, + "The Lens integration has been removed. general_settings.tracing, LITELLM_LENS_* and " + "LENS_GATEWAY_SECRET are ignored. Remove these obsolete settings from your configuration.", + ) in caplog.record_tuples + assert sentinel not in caplog.text + assert secret not in caplog.text + + @pytest.mark.asyncio async def test_proxy_shutdown_commits_spend_before_waiting_on_a_slow_logging_task_and_again_after_it() -> None: """A worker killed while shutdown waits on a slow logging task keeps the spend it had already counted,