litellm/tests/unit/proxy/auth/test_multi_budget_windows.py
devin-ai-integration[bot] 39e31958f8
test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy (#43998)
* test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(proxy): stub HIBP through respx by disabling the aiohttp transport

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(proxy): share the httpx transport fixture across proxy unit tests

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(proxy): restore proxy globals without a missing-value sentinel

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(proxy): package moved dirs and stub the login breach check at the HTTP boundary

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(proxy): isolate the mcp server manager per test

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: yuneng <yuneng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-01 10:11:45 -07:00

137 lines
4.3 KiB
Python

"""
Unit tests for multi-budget-window enforcement on API keys.
"""
from unittest.mock import AsyncMock, patch
import pytest
import litellm
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.auth.auth_checks import _virtual_key_multi_budget_check
def _make_valid_token(**kwargs) -> UserAPIKeyAuth:
defaults = dict(
token="sk-test-token",
key_name="test",
spend=0.0,
max_budget=None,
budget_limits=[],
)
defaults.update(kwargs)
return UserAPIKeyAuth(**defaults)
@pytest.mark.asyncio
async def test_no_budget_limits_passes():
"""Keys with empty budget_limits should pass without raising."""
token = _make_valid_token(budget_limits=[])
# Should not raise
await _virtual_key_multi_budget_check(valid_token=token)
@pytest.mark.asyncio
async def test_under_budget_passes():
"""Key with spend under all windows should pass."""
token = _make_valid_token(
budget_limits=[
{"budget_duration": "24h", "max_budget": 10.0, "reset_at": None},
{"budget_duration": "30d", "max_budget": 100.0, "reset_at": None},
]
)
with patch(
"litellm.proxy.proxy_server.get_current_spend",
new_callable=AsyncMock,
return_value=1.0, # well under both windows
):
await _virtual_key_multi_budget_check(valid_token=token)
@pytest.mark.asyncio
async def test_over_first_window_raises():
"""Key exceeding the first (daily) window should raise BudgetExceededError."""
token = _make_valid_token(
budget_limits=[
{"budget_duration": "24h", "max_budget": 5.0, "reset_at": None},
{"budget_duration": "30d", "max_budget": 100.0, "reset_at": None},
]
)
spend_by_window = [6.0, 6.0] # over daily, under monthly
call_count = 0
async def fake_get_spend(counter_key, fallback_spend, max_budget=None, **kwargs):
nonlocal call_count
val = spend_by_window[call_count]
call_count += 1
return val
with patch(
"litellm.proxy.proxy_server.get_current_spend", side_effect=fake_get_spend
):
with pytest.raises(litellm.BudgetExceededError) as exc_info:
await _virtual_key_multi_budget_check(valid_token=token)
err = exc_info.value
assert err.status_code == 422
assert "24h" in str(err)
assert "Key over" in str(err)
@pytest.mark.asyncio
async def test_over_second_window_raises():
"""Key exceeding only the monthly window should raise BudgetExceededError referencing 30d."""
token = _make_valid_token(
budget_limits=[
{"budget_duration": "24h", "max_budget": 50.0, "reset_at": None},
{"budget_duration": "30d", "max_budget": 5.0, "reset_at": None},
]
)
spend_by_window = [1.0, 10.0] # under daily, over monthly
call_count = 0
async def fake_get_spend(counter_key, fallback_spend, max_budget=None, **kwargs):
nonlocal call_count
val = spend_by_window[call_count]
call_count += 1
return val
with patch(
"litellm.proxy.proxy_server.get_current_spend", side_effect=fake_get_spend
):
with pytest.raises(litellm.BudgetExceededError) as exc_info:
await _virtual_key_multi_budget_check(valid_token=token)
err = exc_info.value
assert err.status_code == 422
assert "30d" in str(err)
@pytest.mark.asyncio
async def test_budget_limit_entry_objects_coerced():
"""BudgetLimitEntry Pydantic objects (not dicts) must be handled without KeyError.
While budget_limits is normally serialized as List[dict], the auth check must
tolerate BudgetLimitEntry objects in case they arrive without prior serialization.
"""
from litellm.proxy._types import BudgetLimitEntry
token = _make_valid_token(budget_limits=[])
# Bypass Pydantic validation to simulate BudgetLimitEntry objects reaching the check
object.__setattr__(
token,
"budget_limits",
[BudgetLimitEntry(budget_duration="24h", max_budget=10.0)],
)
with patch(
"litellm.proxy.proxy_server.get_current_spend",
new_callable=AsyncMock,
return_value=1.0,
):
# Should not raise TypeError / KeyError — model_dump() coerces the object
await _virtual_key_multi_budget_check(valid_token=token)