test: add comprehensive test suite with 89% coverage
- 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>
This commit is contained in:
158
backend/tests/test_logger.py
Normal file
158
backend/tests/test_logger.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user