配置 SSOT(TOML + .env) 统一错误契约 Auth 与事务边界 Redis / Celery 可靠性:业务 Redis(DB/0)与 Celery broker/backend(DB/1)显式拆分;连接池、sync client 可观测性(OpenTelemetry + LGTM)
140 lines
4.8 KiB
Python
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"]
|