Refactor contract of errors
This commit is contained in:
parent
3d134fb7f9
commit
280dabfd95
@ -24,15 +24,13 @@ VALIDATION_PREFIXES = (
|
||||
)
|
||||
|
||||
|
||||
def _error_payload(code: str, message: str, field: str | None = None) -> dict:
|
||||
def _error_payload(code: str | int, message: str, field: str | None = None) -> dict:
|
||||
"""Формирует структурированный payload ошибки по спеке."""
|
||||
payload = {
|
||||
return {
|
||||
"code": code,
|
||||
"field": field,
|
||||
"message": message,
|
||||
}
|
||||
if field:
|
||||
payload["field"] = field
|
||||
return payload
|
||||
|
||||
|
||||
def _parse_prefixed_validation_error(message: str) -> tuple[str, str | None]:
|
||||
@ -80,7 +78,7 @@ def map_sqlalchemy_error(exc: SQLAlchemyError) -> tuple[int, str, str, str | Non
|
||||
return status.HTTP_400_BAD_REQUEST, "db_error", message, None
|
||||
|
||||
|
||||
def map_http_exception(exc: HTTPException) -> tuple[int, str, str, str | None]:
|
||||
def map_http_exception(exc: HTTPException) -> tuple[int, str | int, str, str | None]:
|
||||
"""Маппинг HTTPException в единый формат ошибки API."""
|
||||
status_code = int(exc.status_code)
|
||||
detail = exc.detail
|
||||
@ -90,26 +88,11 @@ def map_http_exception(exc: HTTPException) -> tuple[int, str, str, str | None]:
|
||||
field = detail.get("field")
|
||||
if field is not None:
|
||||
field = str(field)
|
||||
code = detail.get("code")
|
||||
if code is not None:
|
||||
return status_code, str(code), message, field
|
||||
else:
|
||||
message = str(detail) if detail else "HTTP error"
|
||||
field = None
|
||||
code = detail.get("code", status_code)
|
||||
return status_code, code, message, field
|
||||
|
||||
if status_code == status.HTTP_422_UNPROCESSABLE_ENTITY:
|
||||
code, parsed_field = _parse_prefixed_validation_error(message)
|
||||
return status_code, code, message, parsed_field or field
|
||||
if status_code == status.HTTP_401_UNAUTHORIZED:
|
||||
return status_code, "unauthorized", message, field
|
||||
if status_code == status.HTTP_403_FORBIDDEN:
|
||||
return status_code, "access_denied", message, field
|
||||
if status_code == status.HTTP_404_NOT_FOUND:
|
||||
return status_code, "not_found", message, field
|
||||
if status_code == status.HTTP_409_CONFLICT:
|
||||
return status_code, "conflict", message, field
|
||||
|
||||
return status_code, "http_error", message, field
|
||||
message = str(detail) if detail else "HTTP error"
|
||||
return status_code, status_code, message, None
|
||||
|
||||
|
||||
def register_exception_handlers(app: FastAPI) -> None:
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import uuid
|
||||
|
||||
|
||||
def _auth_headers(tokens: dict) -> dict:
|
||||
return {"Authorization": f"Bearer {tokens['access_token']}"}
|
||||
|
||||
@ -8,8 +11,19 @@ def test_users_me(client, admin_tokens):
|
||||
assert response.json()["username"] == "admin"
|
||||
|
||||
|
||||
def test_users_list(client, admin_tokens):
|
||||
listed = client.get("/api/v1/users/", headers=_auth_headers(admin_tokens))
|
||||
assert listed.status_code == 200
|
||||
usernames = [row["username"] for row in listed.json()["result"]]
|
||||
assert "admin" in usernames
|
||||
def test_users_create_smoke(client, admin_tokens):
|
||||
suffix = uuid.uuid4().hex[:8]
|
||||
username = f"user_{suffix}"
|
||||
created = client.put(
|
||||
"/api/v1/users/",
|
||||
json={
|
||||
"email": f"{username}@example.com",
|
||||
"username": username,
|
||||
"password": "pass123",
|
||||
"full_name": "User One",
|
||||
"role_id": 2,
|
||||
},
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
assert created.status_code == 200
|
||||
assert created.json()["result"]["username"] == username
|
||||
|
||||
@ -81,9 +81,9 @@ def test_map_http_exception_validation_prefix():
|
||||
status_code, code, message, field = map_http_exception(exc)
|
||||
|
||||
assert status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert code == "unknown_column"
|
||||
assert code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert message == "unknown_column: q5.plan"
|
||||
assert field == "q5.plan"
|
||||
assert field is None
|
||||
|
||||
|
||||
def test_map_http_exception_from_structured_detail():
|
||||
@ -106,6 +106,17 @@ def test_map_http_exception_unauthorized():
|
||||
status_code, code, message, field = map_http_exception(exc)
|
||||
|
||||
assert status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert code == "unauthorized"
|
||||
assert code == status.HTTP_401_UNAUTHORIZED
|
||||
assert message == "Недействительный токен"
|
||||
assert field is None
|
||||
|
||||
|
||||
def test_map_http_exception_not_found_maps_code_from_status():
|
||||
exc = HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="not found")
|
||||
|
||||
status_code, code, message, field = map_http_exception(exc)
|
||||
|
||||
assert status_code == status.HTTP_404_NOT_FOUND
|
||||
assert code == status.HTTP_404_NOT_FOUND
|
||||
assert message == "not found"
|
||||
assert field is None
|
||||
|
||||
@ -38,7 +38,8 @@ def test_http_exception_returns_structured_payload():
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert response.json() == {
|
||||
"code": "unauthorized",
|
||||
"code": status.HTTP_401_UNAUTHORIZED,
|
||||
"field": None,
|
||||
"message": "Недействительный токен",
|
||||
}
|
||||
|
||||
@ -49,8 +50,8 @@ def test_http_validation_exception_extracts_field():
|
||||
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert response.json() == {
|
||||
"code": "unknown_column",
|
||||
"field": "q5.plan",
|
||||
"code": status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
"field": None,
|
||||
"message": "unknown_column: q5.plan",
|
||||
}
|
||||
|
||||
@ -62,5 +63,6 @@ def test_sqlalchemy_exception_returns_structured_payload():
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.json() == {
|
||||
"code": "not_found",
|
||||
"field": None,
|
||||
"message": "budget_form #99 не существует",
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user