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