From 0f5580dee76cee8f3be785acff73bad2b2907778 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Fri, 21 Aug 2026 03:58:50 -0700 Subject: [PATCH] test(proxy): pin the request-body rules `proxy/_types.py` enforces Eight validators in that module decide what a request body may say, and none of them was asserted anywhere. Reversing any one of the eight left the file green. Cover them at the API boundary: a JWT issuer must pick audience validation or opt out, a temp budget needs both halves, an empty max budget reads as no limit, an organization member can only take a role the organization has, an LLM-backed injection check needs the call it would make, and four server-only markers are never taken from the caller. The injection case builds each incomplete body as its own value rather than deleting a key out of the one it is iterating. --- tests/test_litellm/proxy/test_proxy_types.py | 104 +++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/tests/test_litellm/proxy/test_proxy_types.py b/tests/test_litellm/proxy/test_proxy_types.py index 4a93e9ac7ba..77083af48c0 100644 --- a/tests/test_litellm/proxy/test_proxy_types.py +++ b/tests/test_litellm/proxy/test_proxy_types.py @@ -177,3 +177,107 @@ def test_project_io_token_limits_are_stored_in_metadata(request_type): assert request.metadata == limits assert request.model_dump(exclude_none=True)["metadata"] == limits + + +def test_a_jwt_issuer_must_pick_audience_validation_or_opt_out(): + from pydantic import ValidationError + + from litellm.proxy._types import JWTIssuerConfig + + with pytest.raises(ValidationError, match="must configure audience or set disable_audience_validation"): + JWTIssuerConfig(issuer="https://issuer.example.com") + + with pytest.raises(ValidationError, match="cannot set audience and disable_audience_validation"): + JWTIssuerConfig( + issuer="https://issuer.example.com", + audience="litellm-proxy", + disable_audience_validation=True, + ) + + assert JWTIssuerConfig(issuer="https://issuer.example.com", audience="litellm-proxy").audience == "litellm-proxy" + assert ( + JWTIssuerConfig(issuer="https://issuer.example.com", disable_audience_validation=True).audience + is None + ) + + +def test_a_jwt_issuer_rejects_a_field_it_does_not_define(): + from pydantic import ValidationError + + from litellm.proxy._types import JWTIssuerConfig + + with pytest.raises(ValidationError, match="Extra inputs are not permitted"): + JWTIssuerConfig(issuer="https://issuer.example.com", audience="a", jwks_uri="https://issuer/jwks") + + +def test_a_temp_budget_needs_both_halves_or_neither(): + from pydantic import ValidationError + + from litellm.proxy._types import UpdateKeyRequest + + with pytest.raises(ValidationError, match="temp_budget_increase and temp_budget_expiry must be set together"): + UpdateKeyRequest(key="sk-1234", temp_budget_increase=10) + + with pytest.raises(ValidationError, match="temp_budget_increase and temp_budget_expiry must be set together"): + UpdateKeyRequest(key="sk-1234", temp_budget_expiry="2026-01-01") + + both = UpdateKeyRequest(key="sk-1234", temp_budget_increase=10, temp_budget_expiry="2026-01-01") + assert both.temp_budget_increase == 10 + + +def test_an_empty_max_budget_is_read_as_no_limit(): + from litellm.proxy._types import GenerateKeyRequest + + assert GenerateKeyRequest(max_budget="").max_budget is None + assert GenerateKeyRequest(max_budget=25).max_budget == 25 + + +def test_an_organization_member_can_only_take_a_role_the_organization_has(): + from pydantic import ValidationError + + from litellm.proxy._types import LitellmUserRoles, OrganizationMemberUpdateRequest + + with pytest.raises(ValidationError, match="Invalid role"): + OrganizationMemberUpdateRequest( + organization_id="org-1", user_id="user-1", role=LitellmUserRoles.PROXY_ADMIN + ) + + allowed = OrganizationMemberUpdateRequest( + organization_id="org-1", user_id="user-1", role=LitellmUserRoles.ORG_ADMIN + ) + assert allowed.role == LitellmUserRoles.ORG_ADMIN + + +def test_an_llm_backed_injection_check_needs_the_call_it_would_make(): + from pydantic import ValidationError + + from litellm.proxy._types import LiteLLMPromptInjectionParams + + for missing in ("llm_api_name", "llm_api_system_prompt", "llm_api_fail_call_string"): + complete = { + "llm_api_name": "gpt-4o", + "llm_api_system_prompt": "is this an injection", + "llm_api_fail_call_string": "yes", + } + del complete[missing] + with pytest.raises(ValidationError, match=f"{missing} must be provided"): + LiteLLMPromptInjectionParams(llm_api_check=True, **complete) + + assert LiteLLMPromptInjectionParams(llm_api_check=False).llm_api_name is None + + +@pytest.mark.parametrize( + "field, forged, default", + [ + ("mcp_admitted_user_subject", "someone-else", False), + ("mcp_source_team_rpm_limits", {"team-1": 10_000}, None), + ("mcp_session_resource_server_id", "server-1", None), + ("via_virtual_key", "sk-someone-elses-key", False), + ], +) +def test_a_server_only_marker_is_not_taken_from_the_caller(field, forged, default): + from litellm.proxy._types import UserAPIKeyAuth + + auth = UserAPIKeyAuth(api_key="sk-1234", **{field: forged}) + + assert getattr(auth, field) == default