""" Test cases for spend log cleanup functionality """ from datetime import UTC, datetime, timedelta, timezone from unittest.mock import AsyncMock, MagicMock import pytest from litellm.proxy.db.db_transaction_queue.spend_log_cleanup import SpendLogCleanup @pytest.mark.asyncio async def test_should_delete_spend_logs(): # Test case 1: No retention set cleaner = SpendLogCleanup(general_settings={}) assert cleaner._should_delete_spend_logs() is False # Test case 2: Valid seconds string cleaner = SpendLogCleanup( general_settings={"maximum_spend_logs_retention_period": "3600s"} ) assert cleaner._should_delete_spend_logs() is True # Test case 3: Valid days string cleaner = SpendLogCleanup( general_settings={"maximum_spend_logs_retention_period": "30d"} ) assert cleaner._should_delete_spend_logs() is True # Test case 4: Valid hours string cleaner = SpendLogCleanup( general_settings={"maximum_spend_logs_retention_period": "24h"} ) assert cleaner._should_delete_spend_logs() is True # Test case 5: Invalid format cleaner = SpendLogCleanup( general_settings={"maximum_spend_logs_retention_period": "invalid"} ) assert cleaner._should_delete_spend_logs() is False @pytest.mark.asyncio async def test_cleanup_old_spend_logs_batch_deletion(): from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch # Setup Prisma client mock_prisma_client = MagicMock() mock_db = MagicMock() # Mock spendlogs table mock_spendlogs = MagicMock() mock_spendlogs.find_many = AsyncMock() mock_spendlogs.delete_many = AsyncMock() # Create 1500 mocked logs with .request_id mock_logs = [SimpleNamespace(request_id=f"req_{i}") for i in range(1500)] mock_spendlogs.find_many.side_effect = [ mock_logs[:1000], # Batch 1 mock_logs[1000:], # Batch 2 [], # Done ] # Wire up mocks mock_db.litellm_spendlogs = mock_spendlogs mock_prisma_client.db = mock_db # Mock Redis cache and pod_lock_manager mock_redis_cache = MagicMock() mock_pod_lock_manager = MagicMock() mock_pod_lock_manager.redis_cache = mock_redis_cache mock_pod_lock_manager.acquire_lock = AsyncMock(return_value=True) mock_pod_lock_manager.release_lock = AsyncMock() # Run cleanup with mocked pod_lock_manager test_settings = {"maximum_spend_logs_retention_period": "7d"} cleaner = SpendLogCleanup(general_settings=test_settings) cleaner.pod_lock_manager = mock_pod_lock_manager assert cleaner._should_delete_spend_logs() is True await cleaner.cleanup_old_spend_logs(mock_prisma_client) # Validate batching and deletion assert mock_spendlogs.find_many.call_count == 3 assert mock_spendlogs.delete_many.call_count == 2 mock_spendlogs.delete_many.assert_any_call( where={"request_id": {"in": [f"req_{i}" for i in range(1000)]}} ) mock_spendlogs.delete_many.assert_any_call( where={"request_id": {"in": [f"req_{i}" for i in range(1000, 1500)]}} ) @pytest.mark.asyncio async def test_cleanup_old_spend_logs_retention_period_cutoff(): """ Test that logs are filtered using correct cutoff based on retention """ # Setup Prisma client mock_prisma_client = MagicMock() mock_db = MagicMock() mock_spendlogs = MagicMock() mock_spendlogs.find_many = AsyncMock(return_value=[]) mock_spendlogs.delete_many = AsyncMock() mock_db.litellm_spendlogs = mock_spendlogs mock_prisma_client.db = mock_db # Mock Redis cache and pod_lock_manager mock_redis_cache = MagicMock() mock_pod_lock_manager = MagicMock() mock_pod_lock_manager.redis_cache = mock_redis_cache mock_pod_lock_manager.acquire_lock = AsyncMock(return_value=True) mock_pod_lock_manager.release_lock = AsyncMock() # Run cleanup with mocked pod_lock_manager test_settings = {"maximum_spend_logs_retention_period": "24h"} cleaner = SpendLogCleanup(general_settings=test_settings) cleaner.pod_lock_manager = mock_pod_lock_manager assert cleaner._should_delete_spend_logs() is True await cleaner.cleanup_old_spend_logs(mock_prisma_client) # Verify the cutoff date is correct cutoff_date = mock_spendlogs.find_many.call_args[1]["where"]["startTime"]["lt"] expected_cutoff = datetime.now(timezone.utc) - timedelta(seconds=86400) assert ( abs((cutoff_date - expected_cutoff).total_seconds()) < 1 ) # Allow 1 second difference for test execution time @pytest.mark.asyncio async def test_cleanup_old_spend_logs_no_retention_period(): """ Test that no logs are deleted when no retention period is set """ mock_prisma_client = MagicMock() mock_prisma_client.db.litellm_spendlogs.find_many = AsyncMock() mock_prisma_client.db.litellm_spendlogs.delete = AsyncMock() cleaner = SpendLogCleanup(general_settings={}) # no retention await cleaner.cleanup_old_spend_logs(mock_prisma_client) mock_prisma_client.db.litellm_spendlogs.find_many.assert_not_called() mock_prisma_client.db.litellm_spendlogs.delete.assert_not_called() def test_cleanup_batch_size_env_var(monkeypatch): """Ensure batch size is configurable via environment variable""" import importlib import litellm.constants as constants_module import litellm.proxy.db.db_transaction_queue.spend_log_cleanup as cleanup_module # Set env var and reload modules to pick up new value monkeypatch.setenv("SPEND_LOG_CLEANUP_BATCH_SIZE", "25") importlib.reload(constants_module) importlib.reload(cleanup_module) cleaner = cleanup_module.SpendLogCleanup(general_settings={}) assert cleaner.batch_size == 25 # Remove env var and reload to restore default for other tests monkeypatch.delenv("SPEND_LOG_CLEANUP_BATCH_SIZE", raising=False) importlib.reload(constants_module) importlib.reload(cleanup_module)