feat(deploy): 完成 Phase 3 优化与测试

- T-013 错误处理: 增强 API 错误处理,添加日志记录
- T-015 视频链接: 已在 ResultTable 中实现点击跳转
- T-016 部署配置: 添加前后端 Dockerfile 和 docker-compose.yml
- 新增 11 个错误处理测试用例,共 55 个测试全部通过
- 测试覆盖率达到 93%

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
zfc
2026-01-28 14:42:40 +08:00
co-authored by Claude Opus 4.5
parent 8fbcb72a3f
commit d838a9bea2
9 changed files with 392 additions and 21 deletions
+163
View File
@@ -0,0 +1,163 @@
import pytest
from unittest.mock import patch, AsyncMock, MagicMock
from httpx import AsyncClient, ASGITransport
from sqlalchemy.exc import SQLAlchemyError
from app.main import app
from app.models import KolVideo
from app.api.v1.export import set_export_data, get_export_data
class TestErrorHandling:
"""Tests for error handling scenarios."""
@pytest.fixture
async def client(self, override_get_db):
"""Create test client with dependency override."""
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
@pytest.fixture
async def seed_data(self, test_session, sample_video_data):
"""Seed test data."""
data = sample_video_data.copy()
data["item_id"] = "error_test_001"
data["star_id"] = "error_star_001"
video = KolVideo(**data)
test_session.add(video)
await test_session.commit()
return video
# Query API error handling tests
@patch("app.api.v1.query.query_videos")
@patch("app.api.v1.query.get_brand_names", new_callable=AsyncMock)
async def test_query_database_error(
self, mock_brand, mock_query, client
):
"""Test query returns error on database failure."""
mock_brand.return_value = {}
mock_query.side_effect = SQLAlchemyError("Database connection failed")
response = await client.post(
"/api/v1/query",
json={"type": "star_id", "values": ["test_id"]},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is False
assert "数据库连接失败" in data["error"]
@patch("app.api.v1.query.get_brand_names", new_callable=AsyncMock)
async def test_query_brand_api_failure_fallback(
self, mock_brand, client, test_session, seed_data
):
"""Test query continues with fallback when brand API fails."""
mock_brand.side_effect = Exception("Brand API timeout")
response = await client.post(
"/api/v1/query",
json={"type": "star_id", "values": ["error_star_001"]},
)
assert response.status_code == 200
data = response.json()
# Should still succeed, brand name falls back to brand_id
assert data["success"] is True
assert data["total"] >= 0
@patch("app.api.v1.query.query_videos")
@patch("app.api.v1.query.get_brand_names", new_callable=AsyncMock)
async def test_query_generic_error(
self, mock_brand, mock_query, client
):
"""Test query returns error on unexpected exception."""
mock_brand.return_value = {}
mock_query.side_effect = Exception("Unexpected error")
response = await client.post(
"/api/v1/query",
json={"type": "star_id", "values": ["test_id"]},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is False
assert "Unexpected error" in data["error"]
# Export API error handling tests
async def test_export_no_data_error(self, client):
"""Test export returns error when no data is cached."""
# Clear cached data
set_export_data([])
response = await client.get("/api/v1/export?format=xlsx")
assert response.status_code == 400
data = response.json()
assert data["success"] is False
assert "无数据可导出" in data["error"]
async def test_export_with_data_success(self, client, sample_video_data):
"""Test export succeeds when data is cached."""
# Set cached data
set_export_data([sample_video_data])
response = await client.get("/api/v1/export?format=xlsx")
assert response.status_code == 200
assert "application/vnd.openxmlformats" in response.headers["content-type"]
async def test_export_csv_with_data_success(self, client, sample_video_data):
"""Test CSV export succeeds when data is cached."""
set_export_data([sample_video_data])
response = await client.get("/api/v1/export?format=csv")
assert response.status_code == 200
assert "text/csv" in response.headers["content-type"]
@patch("app.api.v1.export.generate_excel")
async def test_export_generation_error(
self, mock_generate, client, sample_video_data
):
"""Test export returns error when file generation fails."""
mock_generate.side_effect = Exception("Excel generation failed")
set_export_data([sample_video_data])
response = await client.get("/api/v1/export?format=xlsx")
assert response.status_code == 500
data = response.json()
assert data["success"] is False
assert "导出失败" in data["error"]
# Input validation tests
async def test_query_validation_empty_values(self, client):
"""Test query returns 422 for empty values."""
response = await client.post(
"/api/v1/query",
json={"type": "star_id", "values": []},
)
assert response.status_code == 422
async def test_query_validation_invalid_type(self, client):
"""Test query returns 422 for invalid query type."""
response = await client.post(
"/api/v1/query",
json={"type": "invalid_type", "values": ["test"]},
)
assert response.status_code == 422
async def test_query_validation_missing_type(self, client):
"""Test query returns 422 for missing type field."""
response = await client.post(
"/api/v1/query",
json={"values": ["test"]},
)
assert response.status_code == 422
async def test_query_validation_missing_values(self, client):
"""Test query returns 422 for missing values field."""
response = await client.post(
"/api/v1/query",
json={"type": "star_id"},
)
assert response.status_code == 422