- 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>
164 lines
5.9 KiB
Python
164 lines
5.9 KiB
Python
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
|