156 lines
4.9 KiB
Python
156 lines
4.9 KiB
Python
from fastapi import HTTPException, status
|
|
from sqlalchemy.exc import IntegrityError, OperationalError, SQLAlchemyError
|
|
|
|
from src.core.exception_handlers import (
|
|
_error_payload,
|
|
map_http_exception,
|
|
map_sqlalchemy_error,
|
|
)
|
|
|
|
|
|
def test_map_sqlalchemy_error_unique_conflict():
|
|
exc = IntegrityError(
|
|
statement="INSERT INTO users ...",
|
|
params={},
|
|
orig=Exception("duplicate key value violates unique constraint users_username_key"),
|
|
)
|
|
|
|
status_code, code, message, field = map_sqlalchemy_error(exc)
|
|
|
|
assert status_code == status.HTTP_409_CONFLICT
|
|
assert code == "conflict"
|
|
assert "duplicate key value" in message
|
|
assert field is None
|
|
|
|
|
|
def test_map_sqlalchemy_error_check_constraint_is_400():
|
|
exc = IntegrityError(
|
|
statement="UPDATE v3.project ...",
|
|
params={},
|
|
orig=Exception("new row for relation violates check constraint chk_v3_project_name"),
|
|
)
|
|
|
|
status_code, code, message, field = map_sqlalchemy_error(exc)
|
|
|
|
assert status_code == status.HTTP_400_BAD_REQUEST
|
|
assert code == "db_error"
|
|
assert "violates check constraint" in message
|
|
assert field is None
|
|
|
|
|
|
def test_map_sqlalchemy_error_validation_prefix():
|
|
exc = SQLAlchemyError()
|
|
exc.orig = Exception("unknown_column: q5.plan")
|
|
|
|
status_code, code, message, field = map_sqlalchemy_error(exc)
|
|
|
|
assert status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
|
assert code == "unknown_column"
|
|
assert message == "unknown_column: q5.plan"
|
|
assert field == "q5.plan"
|
|
|
|
|
|
def test_map_sqlalchemy_error_not_found():
|
|
exc = SQLAlchemyError()
|
|
exc.orig = Exception("budget_form #99 не существует")
|
|
|
|
status_code, code, message, field = map_sqlalchemy_error(exc)
|
|
|
|
assert status_code == status.HTTP_404_NOT_FOUND
|
|
assert code == "not_found"
|
|
assert "не существует" in message
|
|
assert field is None
|
|
|
|
|
|
def test_map_sqlalchemy_error_role_window_prefixes():
|
|
role_exc = SQLAlchemyError()
|
|
role_exc.orig = Exception("role_not_allowed: q1.m1")
|
|
status_code, code, message, field = map_sqlalchemy_error(role_exc)
|
|
assert status_code == status.HTTP_403_FORBIDDEN
|
|
assert code == "access_denied"
|
|
assert message == "role_not_allowed: q1.m1"
|
|
assert field is None
|
|
|
|
window_exc = SQLAlchemyError()
|
|
window_exc.orig = Exception("window_closed: q1.m1 (closed 2026-05-01T00:00:00Z)")
|
|
status_code, code, message, field = map_sqlalchemy_error(window_exc)
|
|
assert status_code == status.HTTP_403_FORBIDDEN
|
|
assert code == "access_denied"
|
|
assert message.startswith("window_closed:")
|
|
assert field is None
|
|
|
|
|
|
def test_map_sqlalchemy_error_operational():
|
|
exc = OperationalError(
|
|
statement="SELECT 1",
|
|
params={},
|
|
orig=Exception("connection refused"),
|
|
)
|
|
|
|
status_code, code, message, field = map_sqlalchemy_error(exc)
|
|
|
|
assert status_code == status.HTTP_503_SERVICE_UNAVAILABLE
|
|
assert code == "db_unavailable"
|
|
assert message == "Database unavailable"
|
|
assert field is None
|
|
|
|
|
|
def test_error_payload_matches_spec_shape():
|
|
payload = _error_payload("unknown_column", "unknown_column: q5.plan", "q5.plan")
|
|
|
|
assert payload == {
|
|
"code": "unknown_column",
|
|
"field": "q5.plan",
|
|
"message": "unknown_column: q5.plan",
|
|
}
|
|
|
|
|
|
def test_map_http_exception_validation_prefix():
|
|
exc = HTTPException(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
detail="unknown_column: q5.plan",
|
|
)
|
|
|
|
status_code, code, message, field = map_http_exception(exc)
|
|
|
|
assert status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
|
assert code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
|
assert message == "unknown_column: q5.plan"
|
|
assert field is None
|
|
|
|
|
|
def test_map_http_exception_from_structured_detail():
|
|
exc = HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail={"code": "validation_error", "message": "bad input", "field": "username"},
|
|
)
|
|
|
|
status_code, code, message, field = map_http_exception(exc)
|
|
|
|
assert status_code == status.HTTP_400_BAD_REQUEST
|
|
assert code == "validation_error"
|
|
assert message == "bad input"
|
|
assert field == "username"
|
|
|
|
|
|
def test_map_http_exception_unauthorized():
|
|
exc = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Недействительный токен")
|
|
|
|
status_code, code, message, field = map_http_exception(exc)
|
|
|
|
assert status_code == status.HTTP_401_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
|