Files
life-echo/api/tests/test_http_router_error_contract.py
Sully 53e0065e3e refactor(api): TOML 配置 SSOT、统一错误契约、Auth/事务加固与可观测性 (#33)
配置 SSOT(TOML + .env)
统一错误契约
Auth 与事务边界
Redis / Celery 可靠性:业务 Redis(DB/0)与 Celery broker/backend(DB/1)显式拆分;连接池、sync client
可观测性(OpenTelemetry + LGTM)
2026-05-22 13:44:50 +08:00

140 lines
4.8 KiB
Python

"""真实 feature router 的错误契约 HTTP 场景测试。"""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from app.core.errors import BadRequestError, NotFoundError
from app.features.auth.deps import get_auth_service
from app.features.auth.router import router as auth_router
from app.features.auth.service import AuthError
from app.features.conversation.deps import get_conversation_service
from app.features.conversation.router import router as conversation_router
from tests.conftest import install_test_error_handlers
@pytest.mark.asyncio
async def test_auth_register_validation_returns_unified_422() -> None:
app = install_test_error_handlers(FastAPI())
app.include_router(auth_router)
transport = ASGITransport(app=app, raise_app_exceptions=False)
async with AsyncClient(transport=transport, base_url="http://test") as client:
r = await client.post(
"/api/auth/register",
json={"phone": "123", "password": "x", "nickname": ""},
)
assert r.status_code == 422
body = r.json()
assert body["error_code"] == "VALIDATION_ERROR"
assert "request_id" in body
assert body["message"]
@pytest.mark.asyncio
async def test_conversation_list_requires_auth_unified_401() -> None:
app = install_test_error_handlers(FastAPI())
app.include_router(conversation_router)
transport = ASGITransport(app=app, raise_app_exceptions=False)
async with AsyncClient(transport=transport, base_url="http://test") as client:
r = await client.get("/api/conversations")
assert r.status_code == 401
body = r.json()
assert body["error_code"] == "AUTHENTICATION_FAILED"
assert r.headers.get("www-authenticate") == "Bearer"
@pytest.mark.asyncio
async def test_conversation_detail_not_found_unified_404() -> None:
app = install_test_error_handlers(FastAPI())
app.include_router(conversation_router)
mock_service = MagicMock()
mock_service.get_one = AsyncMock(
side_effect=NotFoundError("Conversation not found")
)
app.dependency_overrides[get_conversation_service] = lambda: mock_service
fake_user = MagicMock()
fake_user.id = "user-1"
from app.core.dependencies import get_current_user
app.dependency_overrides[get_current_user] = lambda: fake_user
transport = ASGITransport(app=app, raise_app_exceptions=False)
async with AsyncClient(transport=transport, base_url="http://test") as client:
r = await client.get("/api/conversations/conv-missing")
assert r.status_code == 404
body = r.json()
assert body["error_code"] == "NOT_FOUND"
assert body["message"] == "Conversation not found"
@pytest.mark.asyncio
async def test_auth_login_invalid_credentials_unified_401() -> None:
app = install_test_error_handlers(FastAPI())
app.include_router(auth_router)
mock_service = MagicMock()
mock_service.login = AsyncMock(
side_effect=AuthError("手机号或密码错误", "INVALID_CREDENTIALS")
)
app.dependency_overrides[get_auth_service] = lambda: mock_service
transport = ASGITransport(app=app, raise_app_exceptions=False)
async with AsyncClient(transport=transport, base_url="http://test") as client:
r = await client.post(
"/api/auth/login",
json={
"phone": "13800138000",
"password": "wrong-password",
"agreed_to_terms": True,
},
)
assert r.status_code == 401
body = r.json()
assert body["error_code"] == "AUTHENTICATION_FAILED"
assert body["message"] == "手机号或密码错误"
assert "request_id" in body
assert r.headers.get("www-authenticate") == "Bearer"
@pytest.mark.asyncio
async def test_auth_avatar_invalid_image_unified_400() -> None:
app = install_test_error_handlers(FastAPI())
app.include_router(auth_router)
mock_service = MagicMock()
mock_service.upload_avatar = AsyncMock(
side_effect=BadRequestError("无效的图片文件格式。文件头: deadbeef")
)
app.dependency_overrides[get_auth_service] = lambda: mock_service
fake_user = MagicMock()
fake_user.id = "user-1"
fake_user.avatar_url = None
from app.core.dependencies import get_current_user
app.dependency_overrides[get_current_user] = lambda: fake_user
transport = ASGITransport(app=app, raise_app_exceptions=False)
async with AsyncClient(transport=transport, base_url="http://test") as client:
r = await client.post(
"/api/auth/me/avatar",
files={"file": ("x.png", b"not-an-image", "image/png")},
)
assert r.status_code == 400
body = r.json()
assert body["error_code"] == "BAD_REQUEST"
assert "无效的图片" in body["message"]