Files
litellm/tests/test_litellm/proxy/db/test_create_views.py
T
Yuneng Jiang 9c0b73e5f4 [Fix] should_create_missing_views returns False for reltuples=0 (falsy zero bug)
`should_create_missing_views()` had `and result[0]["reltuples"]` which is
falsy when reltuples=0. On a fresh empty PostgreSQL table, CREATE INDEX sets
reltuples=0, causing the guard to return False and skip view creation entirely.
Views like MonthlyGlobalSpendPerKey are never created, and the
/global/spend/logs endpoint returns 500.

Fix: change to `and result[0]["reltuples"] is not None` so reltuples=0
(empty table) and reltuples=-1 (unanalyzed table) both correctly return True.

Also harden test_vertex_ai.py to return None instead of crashing with
JSONDecodeError when the spend-logs endpoint returns a non-JSON 500 response,
and add unit tests covering all three reltuples branches (0, -1, positive).
2026-04-18 11:00:09 -07:00

192 lines
6.1 KiB
Python

"""
Tests for create_missing_views exception handling fix.
Verifies that real DB errors (auth failures, connection errors, etc.)
are re-raised instead of being silently swallowed, while genuine
"view not found" errors still trigger view creation.
"""
import pytest
from unittest.mock import AsyncMock, MagicMock, call
@pytest.mark.asyncio
async def test_create_views_reraises_connection_error():
"""should re-raise exceptions that are NOT 'does not exist' errors (e.g. connection errors)."""
from litellm.proxy.db.create_views import create_missing_views
mock_db = MagicMock()
mock_db.query_raw = AsyncMock(
side_effect=Exception("connection refused: unable to connect to database")
)
mock_db.execute_raw = AsyncMock()
with pytest.raises(Exception, match="connection refused"):
await create_missing_views(mock_db)
mock_db.execute_raw.assert_not_called()
@pytest.mark.asyncio
async def test_create_views_reraises_permission_error():
"""should re-raise permission denied errors, not treat them as missing views."""
from litellm.proxy.db.create_views import create_missing_views
mock_db = MagicMock()
mock_db.query_raw = AsyncMock(
side_effect=Exception(
"permission denied for table LiteLLM_VerificationTokenView"
)
)
mock_db.execute_raw = AsyncMock()
with pytest.raises(Exception, match="permission denied"):
await create_missing_views(mock_db)
mock_db.execute_raw.assert_not_called()
@pytest.mark.asyncio
async def test_create_views_creates_view_on_does_not_exist():
"""should call execute_raw to create view when error contains 'does not exist'."""
from litellm.proxy.db.create_views import create_missing_views
mock_db = MagicMock()
mock_db.query_raw = AsyncMock(
side_effect=[
Exception('relation "LiteLLM_VerificationTokenView" does not exist'),
None, # MonthlyGlobalSpend exists
None, # Last30dKeysBySpend exists
None, # Last30dModelsBySpend exists
None, # MonthlyGlobalSpendPerKey exists
None, # MonthlyGlobalSpendPerUserPerKey exists
None, # DailyTagSpend exists
None, # Last30dTopEndUsersSpend exists
]
)
mock_db.execute_raw = AsyncMock(return_value=None)
await create_missing_views(mock_db)
mock_db.execute_raw.assert_called_once()
created_sql = mock_db.execute_raw.call_args[0][0]
assert 'CREATE VIEW "LiteLLM_VerificationTokenView"' in created_sql
@pytest.mark.asyncio
async def test_create_views_creates_view_on_undefined_error():
"""should treat 'undefined' errors as 'view not found' and attempt creation."""
from litellm.proxy.db.create_views import create_missing_views
mock_db = MagicMock()
mock_db.query_raw = AsyncMock(
side_effect=[
Exception("undefined table LiteLLM_VerificationTokenView"),
None,
None,
None,
None,
None,
None,
None,
]
)
mock_db.execute_raw = AsyncMock(return_value=None)
await create_missing_views(mock_db)
mock_db.execute_raw.assert_called_once()
@pytest.mark.asyncio
async def test_create_views_skips_creation_when_view_exists():
"""should not call execute_raw when all views already exist."""
from litellm.proxy.db.create_views import create_missing_views
mock_db = MagicMock()
mock_db.query_raw = AsyncMock(return_value=[{"?column?": 1}])
mock_db.execute_raw = AsyncMock()
await create_missing_views(mock_db)
mock_db.execute_raw.assert_not_called()
@pytest.mark.asyncio
async def test_create_views_reraises_undefined_function_error():
"""should re-raise 'undefined function' errors — bare 'undefined' is too broad
and would previously misclassify DB function errors as missing-view signals."""
from litellm.proxy.db.create_views import create_missing_views
mock_db = MagicMock()
mock_db.query_raw = AsyncMock(
side_effect=Exception("ERROR: undefined function pg_get_viewdef()")
)
mock_db.execute_raw = AsyncMock()
with pytest.raises(Exception, match="undefined function"):
await create_missing_views(mock_db)
mock_db.execute_raw.assert_not_called()
@pytest.mark.asyncio
async def test_should_create_missing_views_reltuples_zero():
"""should return True when reltuples is 0 (fresh empty table)."""
from litellm.proxy.db.create_views import should_create_missing_views
mock_db = MagicMock()
mock_db.query_raw = AsyncMock(return_value=[{"reltuples": 0}])
result = await should_create_missing_views(mock_db)
assert result is True
@pytest.mark.asyncio
async def test_should_create_missing_views_reltuples_negative_one():
"""should return True when reltuples is -1 (table created, no ANALYZE yet)."""
from litellm.proxy.db.create_views import should_create_missing_views
mock_db = MagicMock()
mock_db.query_raw = AsyncMock(return_value=[{"reltuples": -1}])
result = await should_create_missing_views(mock_db)
assert result is True
@pytest.mark.asyncio
async def test_should_create_missing_views_reltuples_positive():
"""should return False when reltuples > 0 (table has data)."""
from litellm.proxy.db.create_views import should_create_missing_views
mock_db = MagicMock()
mock_db.query_raw = AsyncMock(return_value=[{"reltuples": 1000}])
result = await should_create_missing_views(mock_db)
assert result is False
@pytest.mark.asyncio
async def test_create_views_creates_view_on_undefined_table_error():
"""should treat 'undefined table' as a missing-view signal and attempt creation."""
from litellm.proxy.db.create_views import create_missing_views
mock_db = MagicMock()
mock_db.query_raw = AsyncMock(
side_effect=[
Exception('undefined table "LiteLLM_VerificationTokenView"'),
None,
None,
None,
None,
None,
None,
None,
]
)
mock_db.execute_raw = AsyncMock(return_value=None)
await create_missing_views(mock_db)
mock_db.execute_raw.assert_called_once()