Files
life-echo/api/tests/test_error_code_registry.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

121 lines
3.7 KiB
Python

"""Ensure runtime error_code values stay within the OpenAPI registry."""
from __future__ import annotations
import inspect
import re
from pathlib import Path
import pytest
from app.core.error_codes import ALL_ERROR_CODES, ERROR_CODE_ENUM
from app.core.errors import (
AppError,
AuthenticationError,
AuthorizationError,
BadRequestError,
ConflictError,
GatewayTimeoutError,
NotFoundError,
ProviderError,
QuotaExceededError,
RateLimitedError,
ServiceUnavailableError,
ValidationError,
_STATUS_TO_ERROR_CODE,
)
from app.features.auth.service import _AUTH_CODE_MAP
from app.features.payment.payment_exceptions import _PAYMENT_CODE_MAP
_APP_FEATURES_ROOT = Path(__file__).resolve().parents[1] / "app" / "features"
_LITERAL_ERROR_CODE_RE = re.compile(r"""error_code\s*=\s*["']([A-Z][A-Z0-9_]*)["']""")
def _app_error_subclass_codes() -> set[str]:
codes: set[str] = set()
for cls in (
NotFoundError,
BadRequestError,
AuthenticationError,
AuthorizationError,
ValidationError,
ConflictError,
ServiceUnavailableError,
GatewayTimeoutError,
ProviderError,
QuotaExceededError,
RateLimitedError,
):
sig = inspect.signature(cls.__init__)
default = sig.parameters.get("message")
# Instantiate with defaults to read resolved error_code from AppError base.
instance = cls()
codes.add(instance.error_code)
return codes
def _auth_runtime_codes() -> set[str]:
return {external for _, external in _AUTH_CODE_MAP.values()}
def _payment_runtime_codes() -> set[str]:
return {external for _, external in _PAYMENT_CODE_MAP.values()}
def _literal_feature_error_codes() -> set[str]:
codes: set[str] = set()
for path in _APP_FEATURES_ROOT.rglob("*.py"):
text = path.read_text(encoding="utf-8")
codes.update(_LITERAL_ERROR_CODE_RE.findall(text))
return codes
def _runtime_error_codes() -> set[str]:
return (
_app_error_subclass_codes()
| _auth_runtime_codes()
| _payment_runtime_codes()
| set(_STATUS_TO_ERROR_CODE.values())
| _literal_feature_error_codes()
)
def test_runtime_error_codes_are_registered_in_openapi_enum() -> None:
runtime = _runtime_error_codes()
registry = set(ERROR_CODE_ENUM)
missing = runtime - registry
assert not missing, f"Unregistered runtime error_code values: {sorted(missing)}"
def test_auth_and_payment_registry_http_status_matches_runtime_maps() -> None:
registry_by_code = {entry["code"]: entry for entry in ALL_ERROR_CODES}
for internal, (status_code, external) in {**_AUTH_CODE_MAP, **_PAYMENT_CODE_MAP}.items():
if external not in registry_by_code:
continue
entry = registry_by_code[external]
assert entry["http_status"] == status_code, (
f"{internal} maps to {external} with HTTP {status_code}, "
f"but registry lists {entry['http_status']}"
)
@pytest.mark.parametrize(
"error_cls,expected_code",
[
(NotFoundError, "NOT_FOUND"),
(BadRequestError, "BAD_REQUEST"),
(AuthenticationError, "AUTHENTICATION_FAILED"),
(AuthorizationError, "FORBIDDEN"),
(ValidationError, "VALIDATION_ERROR"),
(ConflictError, "CONFLICT"),
(ServiceUnavailableError, "SERVICE_UNAVAILABLE"),
(GatewayTimeoutError, "GATEWAY_TIMEOUT"),
(ProviderError, "PROVIDER_ERROR"),
(QuotaExceededError, "QUOTA_EXCEEDED"),
(RateLimitedError, "RATE_LIMITED"),
],
)
def test_app_error_subclasses_use_registered_codes(error_cls: type[AppError], expected_code: str) -> None:
assert error_cls().error_code == expected_code
assert expected_code in ERROR_CODE_ENUM