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:
@@ -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
|
||||
Reference in New Issue
Block a user