feat(backend): 重构 HTTP 错误处理为 RFC7807 标准并优化多个 service

This commit is contained in:
qzl
2026-03-27 14:04:49 +08:00
parent 471488f5f7
commit b1f0eb8921
25 changed files with 1324 additions and 316 deletions
@@ -12,6 +12,8 @@ def test_problem_details_defaults() -> None:
assert result.status == 401
assert result.detail == "Unauthorized"
assert result.instance is None
assert result.code is None
assert result.params is None
def test_problem_details_overrides() -> None:
@@ -21,6 +23,8 @@ def test_problem_details_overrides() -> None:
type_value="https://example.com/problems/conflict",
title="Conflict",
instance="/api/mobile/auth/signup",
code="AUTH_CONFLICT",
params={"field": "email"},
)
assert result.type == "https://example.com/problems/conflict"
@@ -28,3 +32,5 @@ def test_problem_details_overrides() -> None:
assert result.status == 409
assert result.detail == "Conflict"
assert result.instance == "/api/mobile/auth/signup"
assert result.code == "AUTH_CONFLICT"
assert result.params == {"field": "email"}
@@ -4,7 +4,7 @@ from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from fastapi import HTTPException
from core.http.errors import ApiProblemError
from v1.auth.gateway import SupabaseAuthGateway
from v1.auth.schemas import (
@@ -101,7 +101,7 @@ class TestSupabaseAuthGateway:
return_value=SimpleNamespace(session=None, user=None)
)
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(ApiProblemError) as exc_info:
await sut.refresh_session(SessionRefreshRequest(refresh_token="bad"))
assert exc_info.value.status_code == 401
@@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, MagicMock
from uuid import UUID, uuid4
import pytest
from fastapi import HTTPException
from sqlalchemy.exc import SQLAlchemyError
from core.http.errors import ApiProblemError
from models.automation_jobs import AutomationJobStatus, ScheduleType
from v1.automation_jobs.service import (
@@ -203,7 +203,7 @@ class TestCreate:
repository.count_user_jobs.return_value = 0
repository.create.side_effect = SQLAlchemyError("db down")
with pytest.raises(HTTPException) as exc:
with pytest.raises(ApiProblemError) as exc:
await service.create(owner_id, data)
assert exc.value.status_code == 503
@@ -316,7 +316,7 @@ class TestUpdate:
repository.get_by_id.return_value = job
repository.update.side_effect = SQLAlchemyError("db down")
with pytest.raises(HTTPException) as exc:
with pytest.raises(ApiProblemError) as exc:
await service.update(
job.id,
owner_id,
@@ -391,7 +391,7 @@ class TestDelete:
repository.get_by_id.return_value = job
repository.soft_delete.side_effect = SQLAlchemyError("db down")
with pytest.raises(HTTPException) as exc:
with pytest.raises(ApiProblemError) as exc:
await service.delete(job.id, owner_id)
assert exc.value.status_code == 503
@@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, MagicMock
from uuid import UUID, uuid4
import pytest
from fastapi import HTTPException
from core.http.errors import ApiProblemError
from core.auth.models import CurrentUser
from models.friendships import Friendship, FriendshipStatus
@@ -293,7 +293,7 @@ class TestSendRequest:
current_user=current_user,
)
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(ApiProblemError) as exc_info:
await service.send_request(
FriendRequestCreate(target_user_id=current_user.id, content=None)
)
@@ -322,7 +322,7 @@ class TestSendRequest:
current_user=current_user,
)
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(ApiProblemError) as exc_info:
await service.send_request(
FriendRequestCreate(target_user_id=USER_B, content=None)
)
@@ -351,7 +351,7 @@ class TestSendRequest:
current_user=current_user,
)
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(ApiProblemError) as exc_info:
await service.send_request(
FriendRequestCreate(target_user_id=USER_B, content=None)
)
@@ -411,7 +411,7 @@ class TestAcceptRequest:
current_user=current_user,
)
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(ApiProblemError) as exc_info:
await service.accept_request(uuid4())
assert exc_info.value.status_code == 404
@@ -447,7 +447,7 @@ class TestAcceptRequest:
current_user=current_user,
)
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(ApiProblemError) as exc_info:
await service.accept_request(friendship.id)
assert exc_info.value.status_code == 403
@@ -669,7 +669,7 @@ class TestRemoveFriend:
current_user=current_user,
)
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(ApiProblemError) as exc_info:
await service.remove_friend(uuid4())
assert exc_info.value.status_code == 404
@@ -3,10 +3,10 @@ from unittest.mock import AsyncMock, MagicMock
from uuid import UUID, uuid4
import pytest
from fastapi import HTTPException
from sqlalchemy.exc import SQLAlchemyError
from core.auth.models import CurrentUser
from core.http.errors import ApiProblemError
from models.inbox_messages import (
InboxMessage,
InboxMessageStatus as InboxMessageModelStatus,
@@ -109,11 +109,12 @@ async def test_mark_as_read_raises_404_when_message_missing() -> None:
current_user=CurrentUser(id=user_id),
)
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(ApiProblemError) as exc_info:
await service.mark_as_read(message_id)
assert exc_info.value.status_code == 404
assert exc_info.value.detail == "Inbox message not found"
assert exc_info.value.code == "INBOX_MESSAGE_NOT_FOUND"
session.commit.assert_not_awaited()
@@ -133,9 +134,10 @@ async def test_mark_as_read_store_error_returns_503() -> None:
current_user=CurrentUser(id=user_id),
)
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(ApiProblemError) as exc_info:
await service.mark_as_read(message_id)
assert exc_info.value.status_code == 503
assert exc_info.value.detail == "Inbox message store unavailable"
assert exc_info.value.code == "INBOX_MESSAGE_STORE_UNAVAILABLE"
session.rollback.assert_awaited_once()
@@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock
from uuid import UUID, uuid4
import pytest
from fastapi import HTTPException
from core.http.errors import ApiProblemError
from sqlalchemy.exc import SQLAlchemyError
from core.auth.models import CurrentUser
@@ -198,7 +198,7 @@ async def test_create_invalid_end_at(
inbox_repository=mock_inbox_repository,
)
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(ApiProblemError) as exc_info:
await service.create(request)
assert exc_info.value.status_code == 400
@@ -234,7 +234,7 @@ async def test_get_by_id_not_found(
inbox_repository=mock_inbox_repository,
)
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(ApiProblemError) as exc_info:
await service.get_by_id(uuid4())
assert exc_info.value.status_code == 404
@@ -489,7 +489,7 @@ async def test_list_by_date_range_rolls_back_when_query_fails_after_archive(
inbox_repository=mock_inbox_repository,
)
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(ApiProblemError) as exc_info:
await service.list_by_date_range(
request=ScheduleItemListRequest(
start_at=datetime(2026, 2, 1, 0, 0, tzinfo=timezone.utc),
@@ -3,7 +3,7 @@ from __future__ import annotations
from uuid import UUID
import pytest
from fastapi import HTTPException
from core.http.errors import ApiProblemError
from core.auth.jwt_verifier import TokenValidationError
import v1.users.dependencies as deps
@@ -49,7 +49,7 @@ async def test_get_current_user_raises_401_when_fallback_fails(monkeypatch) -> N
monkeypatch.setattr(deps, "_verify_user_with_supabase", _fallback)
with pytest.raises(HTTPException) as exc:
with pytest.raises(ApiProblemError) as exc:
await deps.get_current_user(authorization="Bearer invalid-token")
assert exc.value.status_code == 401