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:
2026-03-11 19:12:57 +08:00
parent 26738973bd
commit d080e67c29
19 changed files with 1849 additions and 6 deletions

168
backend/tests/conftest.py Normal file
View File

@@ -0,0 +1,168 @@
"""Test configuration and shared fixtures."""
import asyncio
import pytest
import aiosqlite
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock
from contextlib import asynccontextmanager
# Test database path
TEST_DB_PATH = Path(__file__).parent / "data" / "test.db"
@asynccontextmanager
async def get_test_db():
"""Get in-memory test database connection."""
conn = await aiosqlite.connect(":memory:")
conn.row_factory = aiosqlite.Row
await _init_test_schema(conn)
await _seed_test_data(conn)
yield conn
await conn.close()
async def _init_test_schema(db: aiosqlite.Connection):
"""Initialize test database schema."""
await db.executescript("""
CREATE TABLE IF NOT EXISTS client_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT UNIQUE NOT NULL,
name TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS providers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
base_url TEXT NOT NULL,
api_type TEXT NOT NULL,
is_active BOOLEAN DEFAULT TRUE
);
CREATE TABLE IF NOT EXISTS provider_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider_id INTEGER NOT NULL REFERENCES providers(id),
key TEXT NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS request_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_key_id INTEGER REFERENCES client_keys(id),
provider_id INTEGER REFERENCES providers(id),
model TEXT,
prompt_tokens INTEGER,
completion_tokens INTEGER,
latency_ms INTEGER,
success BOOLEAN,
error_message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
""")
async def _seed_test_data(db: aiosqlite.Connection):
"""Seed test data."""
# Seed providers
await db.executemany(
"INSERT INTO providers (name, base_url, api_type) VALUES (?, ?, ?)",
[
("openai", "https://api.openai.com/v1", "openai"),
("anthropic", "https://api.anthropic.com/v1", "anthropic"),
]
)
# Seed client keys
await db.execute(
"INSERT INTO client_keys (key, name, is_active) VALUES (?, ?, ?)",
("test-api-key", "test-client", True)
)
await db.execute(
"INSERT INTO client_keys (key, name, is_active) VALUES (?, ?, ?)",
("disabled-key", "disabled-client", False)
)
# Seed provider keys
await db.execute(
"INSERT INTO provider_keys (provider_id, key, is_active) VALUES (?, ?, ?)",
(1, "sk-openai-key-1", True)
)
await db.execute(
"INSERT INTO provider_keys (provider_id, key, is_active) VALUES (?, ?, ?)",
(1, "sk-openai-key-2", True)
)
await db.execute(
"INSERT INTO provider_keys (provider_id, key, is_active) VALUES (?, ?, ?)",
(2, "sk-ant-key-1", True)
)
await db.commit()
@pytest.fixture
def mock_db(monkeypatch):
"""Mock database connection for tests."""
@asynccontextmanager
async def mock_db_connection():
async with get_test_db() as db:
yield db
monkeypatch.setattr("app.database.db_connection", mock_db_connection)
return mock_db_connection
@pytest.fixture
def mock_litellm():
"""Mock LiteLLM completion."""
mock_response = MagicMock()
mock_response.usage = MagicMock()
mock_response.usage.prompt_tokens = 10
mock_response.usage.completion_tokens = 20
mock_response.model_dump.return_value = {
"id": "chatcmpl-test",
"object": "chat.completion",
"choices": [{"message": {"content": "test response"}}]
}
async def mock_acompletion(*args, **kwargs):
return mock_response
return {"acompletion": mock_acompletion, "response": mock_response}
@pytest.fixture
def client_key():
"""Return a test client key instance."""
from app.models import ClientKey
return ClientKey(
id=1,
key="test-api-key",
name="test-client",
is_active=True,
created_at="2024-01-01 00:00:00"
)
@pytest.fixture
def provider():
"""Return a test provider instance."""
from app.models import Provider
return Provider(
id=1,
name="openai",
base_url="https://api.openai.com/v1",
api_type="openai",
is_active=True
)
@pytest.fixture
def provider_key():
"""Return a test provider key instance."""
from app.models import ProviderKey
return ProviderKey(
id=1,
provider_id=1,
key="sk-test-key",
is_active=True,
created_at="2024-01-01 00:00:00"
)