- Add tests for all backend modules: config, database, models, services, routes, middleware, and providers - Separate dev dependencies using dependency-groups - Update CLAUDE.md with test commands and project structure - Add .coverage and .pytest_cache to gitignore 88 tests covering: - Authentication and authorization - Model routing and key selection - Async logging with batch processing - All API endpoints (chat, openai, anthropic, health) - LiteLLM wrapper (mocked external calls) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
159 lines
4.4 KiB
Python
159 lines
4.4 KiB
Python
"""Tests for async logger service."""
|
|
import pytest
|
|
import asyncio
|
|
from unittest.mock import patch, AsyncMock, MagicMock
|
|
from app.services.logger import AsyncLogger, LogEntry, async_logger
|
|
|
|
|
|
class TestLogEntry:
|
|
"""Test LogEntry dataclass."""
|
|
|
|
def test_create_log_entry(self):
|
|
"""Test creating a LogEntry."""
|
|
entry = LogEntry(
|
|
client_key_id=1,
|
|
provider_id=2,
|
|
model="gpt-4o",
|
|
prompt_tokens=10,
|
|
completion_tokens=20,
|
|
latency_ms=100,
|
|
success=True
|
|
)
|
|
assert entry.client_key_id == 1
|
|
assert entry.model == "gpt-4o"
|
|
assert entry.success is True
|
|
|
|
def test_log_entry_with_error(self):
|
|
"""Test LogEntry with error."""
|
|
entry = LogEntry(
|
|
client_key_id=1,
|
|
provider_id=2,
|
|
model="gpt-4o",
|
|
success=False,
|
|
error_message="Rate limit exceeded"
|
|
)
|
|
assert entry.success is False
|
|
assert entry.error_message == "Rate limit exceeded"
|
|
|
|
|
|
class TestAsyncLogger:
|
|
"""Test AsyncLogger."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_creates_task(self):
|
|
"""Test start creates background task."""
|
|
logger = AsyncLogger()
|
|
assert logger._task is None
|
|
|
|
await logger.start()
|
|
assert logger._task is not None
|
|
|
|
await logger.stop()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stop_cancels_task(self):
|
|
"""Test stop cancels background task."""
|
|
logger = AsyncLogger()
|
|
await logger.start()
|
|
assert logger._task is not None
|
|
|
|
await logger.stop()
|
|
assert logger._task is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_queues_entry(self):
|
|
"""Test log queues an entry."""
|
|
logger = AsyncLogger()
|
|
entry = LogEntry(
|
|
client_key_id=1,
|
|
provider_id=2,
|
|
model="gpt-4o",
|
|
success=True
|
|
)
|
|
|
|
await logger.log(entry)
|
|
assert not logger._queue.empty()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_flush_batch_empty_queue(self, mock_db):
|
|
"""Test flush with empty queue."""
|
|
logger = AsyncLogger()
|
|
with patch("app.services.logger.db_connection", mock_db):
|
|
await logger._flush_batch() # Should not raise
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_flush_batch_with_entries(self, mock_db):
|
|
"""Test flush with entries in queue."""
|
|
logger = AsyncLogger()
|
|
entry = LogEntry(
|
|
client_key_id=1,
|
|
provider_id=2,
|
|
model="gpt-4o",
|
|
prompt_tokens=10,
|
|
completion_tokens=20,
|
|
latency_ms=100,
|
|
success=True
|
|
)
|
|
|
|
await logger.log(entry)
|
|
|
|
with patch("app.services.logger.db_connection", mock_db):
|
|
await logger._flush_batch()
|
|
|
|
assert logger._queue.empty()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_flush_all(self, mock_db):
|
|
"""Test flush_all drains all entries."""
|
|
logger = AsyncLogger()
|
|
|
|
# Add multiple entries
|
|
for i in range(5):
|
|
entry = LogEntry(
|
|
client_key_id=1,
|
|
provider_id=2,
|
|
model=f"model-{i}",
|
|
success=True
|
|
)
|
|
await logger.log(entry)
|
|
|
|
with patch("app.services.logger.db_connection", mock_db):
|
|
await logger._flush_all()
|
|
|
|
assert logger._queue.empty()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_stop_cycle(self):
|
|
"""Test full start/stop cycle."""
|
|
logger = AsyncLogger()
|
|
|
|
await logger.start()
|
|
await asyncio.sleep(0.1) # Let task run briefly
|
|
await logger.stop()
|
|
|
|
assert logger._task is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multiple_start_calls_safe(self):
|
|
"""Test multiple start calls are safe."""
|
|
logger = AsyncLogger()
|
|
|
|
await logger.start()
|
|
first_task = logger._task
|
|
await logger.start() # Should not create new task
|
|
|
|
assert logger._task is first_task
|
|
|
|
await logger.stop()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stop_without_start_safe(self):
|
|
"""Test stop without start is safe."""
|
|
logger = AsyncLogger()
|
|
await logger.stop() # Should not raise
|
|
|
|
def test_global_instance_exists(self):
|
|
"""Test global async_logger instance exists."""
|
|
assert async_logger is not None
|
|
assert isinstance(async_logger, AsyncLogger)
|