mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-03-02 17:30:48 +01:00
767 lines
26 KiB
Python
767 lines
26 KiB
Python
"""Tests for fastapi_toolsets.exceptions module."""
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.exceptions import HTTPException
|
|
from fastapi.testclient import TestClient
|
|
|
|
from fastapi_toolsets.exceptions import (
|
|
ApiException,
|
|
ConflictError,
|
|
ForbiddenError,
|
|
InvalidOrderFieldError,
|
|
NotFoundError,
|
|
UnauthorizedError,
|
|
generate_error_responses,
|
|
init_exceptions_handlers,
|
|
)
|
|
from fastapi_toolsets.schemas import ApiError
|
|
|
|
|
|
class TestApiException:
|
|
"""Tests for ApiException base class."""
|
|
|
|
def test_subclass_with_api_error(self):
|
|
"""Subclasses can define api_error."""
|
|
|
|
class CustomError(ApiException):
|
|
api_error = ApiError(
|
|
code=418,
|
|
msg="I'm a teapot",
|
|
desc="The server is a teapot.",
|
|
err_code="TEA-418",
|
|
)
|
|
|
|
error = CustomError()
|
|
assert error.api_error.code == 418
|
|
assert error.api_error.msg == "I'm a teapot"
|
|
assert str(error) == "I'm a teapot"
|
|
|
|
def test_detail_overrides_msg_and_str(self):
|
|
"""detail sets both str(exc) and api_error.msg; class-level msg is unchanged."""
|
|
|
|
class CustomError(ApiException):
|
|
api_error = ApiError(
|
|
code=400,
|
|
msg="Bad Request",
|
|
desc="Request was bad.",
|
|
err_code="BAD-400",
|
|
)
|
|
|
|
error = CustomError("Widget not found")
|
|
assert str(error) == "Widget not found"
|
|
assert error.api_error.msg == "Widget not found"
|
|
assert CustomError.api_error.msg == "Bad Request" # class unchanged
|
|
|
|
def test_desc_override(self):
|
|
"""desc kwarg overrides api_error.desc on the instance only."""
|
|
|
|
class MyError(ApiException):
|
|
api_error = ApiError(
|
|
code=400, msg="Error", desc="Default.", err_code="ERR-400"
|
|
)
|
|
|
|
err = MyError(desc="Custom desc.")
|
|
assert err.api_error.desc == "Custom desc."
|
|
assert MyError.api_error.desc == "Default." # class unchanged
|
|
|
|
def test_data_override(self):
|
|
"""data kwarg sets api_error.data on the instance only."""
|
|
|
|
class MyError(ApiException):
|
|
api_error = ApiError(
|
|
code=400, msg="Error", desc="Default.", err_code="ERR-400"
|
|
)
|
|
|
|
err = MyError(data={"key": "value"})
|
|
assert err.api_error.data == {"key": "value"}
|
|
assert MyError.api_error.data is None # class unchanged
|
|
|
|
def test_desc_and_data_override(self):
|
|
"""detail, desc and data can all be overridden together."""
|
|
|
|
class MyError(ApiException):
|
|
api_error = ApiError(
|
|
code=400, msg="Error", desc="Default.", err_code="ERR-400"
|
|
)
|
|
|
|
err = MyError("custom msg", desc="New desc.", data={"x": 1})
|
|
assert str(err) == "custom msg"
|
|
assert err.api_error.msg == "custom msg" # detail also updates msg
|
|
assert err.api_error.desc == "New desc."
|
|
assert err.api_error.data == {"x": 1}
|
|
assert err.api_error.code == 400 # other fields unchanged
|
|
|
|
def test_class_api_error_not_mutated_after_instance_override(self):
|
|
"""Raising with desc/data does not mutate the class-level api_error."""
|
|
|
|
class MyError(ApiException):
|
|
api_error = ApiError(
|
|
code=400, msg="Error", desc="Default.", err_code="ERR-400"
|
|
)
|
|
|
|
MyError(desc="Changed", data={"x": 1})
|
|
assert MyError.api_error.desc == "Default."
|
|
assert MyError.api_error.data is None
|
|
|
|
def test_subclass_uses_super_with_desc_and_data(self):
|
|
"""Subclasses can delegate detail/desc/data to super().__init__()."""
|
|
|
|
class BuildValidationError(ApiException):
|
|
api_error = ApiError(
|
|
code=422,
|
|
msg="Build Validation Error",
|
|
desc="The build configuration is invalid.",
|
|
err_code="BUILD-422",
|
|
)
|
|
|
|
def __init__(self, *errors: str) -> None:
|
|
super().__init__(
|
|
f"{len(errors)} validation error(s)",
|
|
desc=", ".join(errors),
|
|
data={"errors": [{"message": e} for e in errors]},
|
|
)
|
|
|
|
err = BuildValidationError("Field A is required", "Field B is invalid")
|
|
assert str(err) == "2 validation error(s)"
|
|
assert err.api_error.msg == "2 validation error(s)" # detail set msg
|
|
assert err.api_error.desc == "Field A is required, Field B is invalid"
|
|
assert err.api_error.data == {
|
|
"errors": [
|
|
{"message": "Field A is required"},
|
|
{"message": "Field B is invalid"},
|
|
]
|
|
}
|
|
assert err.api_error.code == 422 # other fields unchanged
|
|
|
|
def test_detail_desc_data_in_http_response(self):
|
|
"""detail/desc/data overrides all appear correctly in the FastAPI HTTP response."""
|
|
|
|
class DynamicError(ApiException):
|
|
api_error = ApiError(
|
|
code=400, msg="Error", desc="Default.", err_code="ERR-400"
|
|
)
|
|
|
|
def __init__(self, message: str) -> None:
|
|
super().__init__(
|
|
message,
|
|
desc=f"Detail: {message}",
|
|
data={"reason": message},
|
|
)
|
|
|
|
app = FastAPI()
|
|
init_exceptions_handlers(app)
|
|
|
|
@app.get("/error")
|
|
async def raise_error():
|
|
raise DynamicError("something went wrong")
|
|
|
|
client = TestClient(app)
|
|
response = client.get("/error")
|
|
|
|
assert response.status_code == 400
|
|
body = response.json()
|
|
assert body["message"] == "something went wrong"
|
|
assert body["description"] == "Detail: something went wrong"
|
|
assert body["data"] == {"reason": "something went wrong"}
|
|
|
|
|
|
class TestApiExceptionGuard:
|
|
"""Tests for the __init_subclass__ api_error guard."""
|
|
|
|
def test_missing_api_error_raises_type_error(self):
|
|
"""Defining a subclass without api_error raises TypeError at class creation time."""
|
|
with pytest.raises(
|
|
TypeError, match="must define an 'api_error' class attribute"
|
|
):
|
|
|
|
class BrokenError(ApiException):
|
|
pass
|
|
|
|
def test_abstract_subclass_skips_guard(self):
|
|
"""abstract=True allows intermediate base classes without api_error."""
|
|
|
|
class BaseGroupError(ApiException, abstract=True):
|
|
pass
|
|
|
|
# Concrete child must still define it
|
|
class ConcreteError(BaseGroupError):
|
|
api_error = ApiError(
|
|
code=400, msg="Error", desc="Desc.", err_code="ERR-400"
|
|
)
|
|
|
|
err = ConcreteError()
|
|
assert err.api_error.code == 400
|
|
|
|
def test_abstract_child_still_requires_api_error_on_concrete(self):
|
|
"""Concrete subclass of an abstract class must define api_error."""
|
|
|
|
class Base(ApiException, abstract=True):
|
|
pass
|
|
|
|
with pytest.raises(
|
|
TypeError, match="must define an 'api_error' class attribute"
|
|
):
|
|
|
|
class Concrete(Base):
|
|
pass
|
|
|
|
def test_inherited_api_error_satisfies_guard(self):
|
|
"""Subclass that inherits api_error from a parent does not need its own."""
|
|
|
|
class ConcreteError(NotFoundError):
|
|
pass
|
|
|
|
err = ConcreteError()
|
|
assert err.api_error.code == 404
|
|
|
|
|
|
class TestBuiltInExceptions:
|
|
"""Tests for built-in exception classes."""
|
|
|
|
def test_unauthorized_error(self):
|
|
"""UnauthorizedError has correct attributes."""
|
|
error = UnauthorizedError()
|
|
assert error.api_error.code == 401
|
|
assert error.api_error.err_code == "AUTH-401"
|
|
|
|
def test_forbidden_error(self):
|
|
"""ForbiddenError has correct attributes."""
|
|
error = ForbiddenError()
|
|
assert error.api_error.code == 403
|
|
assert error.api_error.err_code == "AUTH-403"
|
|
|
|
def test_not_found_error(self):
|
|
"""NotFoundError has correct attributes."""
|
|
error = NotFoundError()
|
|
assert error.api_error.code == 404
|
|
assert error.api_error.err_code == "RES-404"
|
|
|
|
def test_conflict_error(self):
|
|
"""ConflictError has correct attributes."""
|
|
error = ConflictError()
|
|
assert error.api_error.code == 409
|
|
assert error.api_error.err_code == "RES-409"
|
|
|
|
|
|
class TestGenerateErrorResponses:
|
|
"""Tests for generate_error_responses function."""
|
|
|
|
def test_generates_single_response(self):
|
|
"""Generates response for single exception."""
|
|
responses = generate_error_responses(NotFoundError)
|
|
|
|
assert 404 in responses
|
|
assert responses[404]["description"] == "Not Found"
|
|
|
|
def test_generates_multiple_responses(self):
|
|
"""Generates responses for multiple exceptions with distinct status codes."""
|
|
responses = generate_error_responses(
|
|
UnauthorizedError,
|
|
ForbiddenError,
|
|
NotFoundError,
|
|
)
|
|
|
|
assert 401 in responses
|
|
assert 403 in responses
|
|
assert 404 in responses
|
|
|
|
def test_response_has_named_example(self):
|
|
"""Generated response uses named examples keyed by err_code."""
|
|
responses = generate_error_responses(NotFoundError)
|
|
examples = responses[404]["content"]["application/json"]["examples"]
|
|
|
|
assert "RES-404" in examples
|
|
value = examples["RES-404"]["value"]
|
|
assert value["status"] == "FAIL"
|
|
assert value["error_code"] == "RES-404"
|
|
assert value["message"] == "Not Found"
|
|
assert value["data"] is None
|
|
|
|
def test_response_example_has_summary(self):
|
|
"""Each named example carries a summary equal to api_error.msg."""
|
|
responses = generate_error_responses(NotFoundError)
|
|
example = responses[404]["content"]["application/json"]["examples"]["RES-404"]
|
|
|
|
assert example["summary"] == "Not Found"
|
|
|
|
def test_response_example_with_data(self):
|
|
"""Generated response includes data when set on ApiError."""
|
|
|
|
class ErrorWithData(ApiException):
|
|
api_error = ApiError(
|
|
code=400,
|
|
msg="Bad Request",
|
|
desc="Invalid input.",
|
|
err_code="BAD-400",
|
|
data={"details": "some context"},
|
|
)
|
|
|
|
responses = generate_error_responses(ErrorWithData)
|
|
value = responses[400]["content"]["application/json"]["examples"]["BAD-400"][
|
|
"value"
|
|
]
|
|
|
|
assert value["data"] == {"details": "some context"}
|
|
|
|
def test_two_errors_same_code_both_present(self):
|
|
"""Two exceptions with the same HTTP code produce two named examples."""
|
|
|
|
class BadRequestA(ApiException):
|
|
api_error = ApiError(
|
|
code=400, msg="Bad A", desc="Reason A.", err_code="ERR-A"
|
|
)
|
|
|
|
class BadRequestB(ApiException):
|
|
api_error = ApiError(
|
|
code=400, msg="Bad B", desc="Reason B.", err_code="ERR-B"
|
|
)
|
|
|
|
responses = generate_error_responses(BadRequestA, BadRequestB)
|
|
|
|
assert 400 in responses
|
|
examples = responses[400]["content"]["application/json"]["examples"]
|
|
assert "ERR-A" in examples
|
|
assert "ERR-B" in examples
|
|
assert examples["ERR-A"]["value"]["message"] == "Bad A"
|
|
assert examples["ERR-B"]["value"]["message"] == "Bad B"
|
|
|
|
def test_two_errors_same_code_single_top_level_entry(self):
|
|
"""Two exceptions with the same HTTP code produce exactly one top-level entry."""
|
|
|
|
class BadRequestA(ApiException):
|
|
api_error = ApiError(
|
|
code=400, msg="Bad A", desc="Reason A.", err_code="ERR-A"
|
|
)
|
|
|
|
class BadRequestB(ApiException):
|
|
api_error = ApiError(
|
|
code=400, msg="Bad B", desc="Reason B.", err_code="ERR-B"
|
|
)
|
|
|
|
responses = generate_error_responses(BadRequestA, BadRequestB)
|
|
assert len([k for k in responses if k == 400]) == 1
|
|
|
|
|
|
class TestInitExceptionsHandlers:
|
|
"""Tests for init_exceptions_handlers function."""
|
|
|
|
def test_returns_app(self):
|
|
"""Returns the FastAPI app."""
|
|
app = FastAPI()
|
|
result = init_exceptions_handlers(app)
|
|
assert result is app
|
|
|
|
def test_handles_api_exception(self):
|
|
"""Handles ApiException with structured response."""
|
|
app = FastAPI()
|
|
init_exceptions_handlers(app)
|
|
|
|
@app.get("/error")
|
|
async def raise_error():
|
|
raise NotFoundError()
|
|
|
|
client = TestClient(app)
|
|
response = client.get("/error")
|
|
|
|
assert response.status_code == 404
|
|
data = response.json()
|
|
assert data["status"] == "FAIL"
|
|
assert data["error_code"] == "RES-404"
|
|
assert data["message"] == "Not Found"
|
|
|
|
def test_handles_api_exception_without_data(self):
|
|
"""ApiException without data returns null data field."""
|
|
app = FastAPI()
|
|
init_exceptions_handlers(app)
|
|
|
|
@app.get("/error")
|
|
async def raise_error():
|
|
raise NotFoundError()
|
|
|
|
client = TestClient(app)
|
|
response = client.get("/error")
|
|
|
|
assert response.status_code == 404
|
|
assert response.json()["data"] is None
|
|
|
|
def test_handles_api_exception_with_data(self):
|
|
"""ApiException with data returns the data payload."""
|
|
app = FastAPI()
|
|
init_exceptions_handlers(app)
|
|
|
|
class CustomValidationError(ApiException):
|
|
api_error = ApiError(
|
|
code=422,
|
|
msg="Validation Error",
|
|
desc="1 validation error(s) detected",
|
|
err_code="CUSTOM-422",
|
|
data={
|
|
"errors": [
|
|
{
|
|
"field": "email",
|
|
"message": "invalid format",
|
|
"type": "value_error",
|
|
}
|
|
]
|
|
},
|
|
)
|
|
|
|
@app.get("/error")
|
|
async def raise_error():
|
|
raise CustomValidationError()
|
|
|
|
client = TestClient(app)
|
|
response = client.get("/error")
|
|
|
|
assert response.status_code == 422
|
|
data = response.json()
|
|
assert data["data"] == {
|
|
"errors": [
|
|
{"field": "email", "message": "invalid format", "type": "value_error"}
|
|
]
|
|
}
|
|
assert data["error_code"] == "CUSTOM-422"
|
|
|
|
def test_handles_validation_error(self):
|
|
"""Handles validation errors with structured response."""
|
|
from pydantic import BaseModel
|
|
|
|
app = FastAPI()
|
|
init_exceptions_handlers(app)
|
|
|
|
class Item(BaseModel):
|
|
name: str
|
|
price: float
|
|
|
|
@app.post("/items")
|
|
async def create_item(item: Item):
|
|
return item
|
|
|
|
client = TestClient(app)
|
|
response = client.post("/items", json={"name": 123})
|
|
|
|
assert response.status_code == 422
|
|
data = response.json()
|
|
assert data["status"] == "FAIL"
|
|
assert data["error_code"] == "VAL-422"
|
|
assert "errors" in data["data"]
|
|
|
|
def test_handles_generic_exception(self):
|
|
"""Handles unhandled exceptions with 500 response."""
|
|
app = FastAPI()
|
|
init_exceptions_handlers(app)
|
|
|
|
@app.get("/crash")
|
|
async def crash():
|
|
raise RuntimeError("Something went wrong")
|
|
|
|
client = TestClient(app, raise_server_exceptions=False)
|
|
response = client.get("/crash")
|
|
|
|
assert response.status_code == 500
|
|
data = response.json()
|
|
assert data["status"] == "FAIL"
|
|
assert data["error_code"] == "SERVER-500"
|
|
|
|
def test_handles_http_exception(self):
|
|
"""Handles starlette HTTPException with consistent ErrorResponse envelope."""
|
|
app = FastAPI()
|
|
init_exceptions_handlers(app)
|
|
|
|
@app.get("/protected")
|
|
async def protected():
|
|
raise HTTPException(status_code=403, detail="Forbidden")
|
|
|
|
client = TestClient(app)
|
|
response = client.get("/protected")
|
|
|
|
assert response.status_code == 403
|
|
data = response.json()
|
|
assert data["status"] == "FAIL"
|
|
assert data["error_code"] == "HTTP-403"
|
|
assert data["message"] == "Forbidden"
|
|
|
|
def test_handles_http_exception_404_from_route(self):
|
|
"""HTTPException(404) raised inside a route uses the consistent ErrorResponse envelope."""
|
|
app = FastAPI()
|
|
init_exceptions_handlers(app)
|
|
|
|
@app.get("/items/{item_id}")
|
|
async def get_item(item_id: int):
|
|
raise HTTPException(status_code=404, detail="Item not found")
|
|
|
|
client = TestClient(app)
|
|
response = client.get("/items/99")
|
|
|
|
assert response.status_code == 404
|
|
data = response.json()
|
|
assert data["status"] == "FAIL"
|
|
assert data["error_code"] == "HTTP-404"
|
|
assert data["message"] == "Item not found"
|
|
|
|
def test_handles_http_exception_forwards_headers(self):
|
|
"""HTTPException with WWW-Authenticate header forwards it in the response."""
|
|
app = FastAPI()
|
|
init_exceptions_handlers(app)
|
|
|
|
@app.get("/secure")
|
|
async def secure():
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail="Not authenticated",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
client = TestClient(app)
|
|
response = client.get("/secure")
|
|
|
|
assert response.status_code == 401
|
|
assert response.headers.get("www-authenticate") == "Bearer"
|
|
|
|
def test_custom_openapi_schema(self):
|
|
"""Customises OpenAPI schema for 422 responses using named examples."""
|
|
from pydantic import BaseModel
|
|
|
|
app = FastAPI()
|
|
init_exceptions_handlers(app)
|
|
|
|
class Item(BaseModel):
|
|
name: str
|
|
|
|
@app.post("/items")
|
|
async def create_item(item: Item):
|
|
return item
|
|
|
|
openapi = app.openapi()
|
|
|
|
post_op = openapi["paths"]["/items"]["post"]
|
|
assert "422" in post_op["responses"]
|
|
resp_422 = post_op["responses"]["422"]
|
|
examples = resp_422["content"]["application/json"]["examples"]
|
|
assert "VAL-422" in examples
|
|
assert examples["VAL-422"]["value"]["error_code"] == "VAL-422"
|
|
|
|
def test_custom_openapi_preserves_app_metadata(self):
|
|
"""_patched_openapi preserves custom FastAPI app-level metadata."""
|
|
app = FastAPI(
|
|
title="My API",
|
|
version="2.0.0",
|
|
description="Custom description",
|
|
)
|
|
init_exceptions_handlers(app)
|
|
|
|
schema = app.openapi()
|
|
assert schema["info"]["title"] == "My API"
|
|
assert schema["info"]["version"] == "2.0.0"
|
|
|
|
def test_handles_response_validation_error(self):
|
|
"""Handles ResponseValidationError with a structured 422 response."""
|
|
from pydantic import BaseModel
|
|
|
|
class CountResponse(BaseModel):
|
|
count: int
|
|
|
|
app = FastAPI()
|
|
init_exceptions_handlers(app)
|
|
|
|
@app.get("/broken", response_model=CountResponse)
|
|
async def broken():
|
|
return {"count": "not-a-number"} # triggers ResponseValidationError
|
|
|
|
client = TestClient(app, raise_server_exceptions=False)
|
|
response = client.get("/broken")
|
|
|
|
assert response.status_code == 422
|
|
data = response.json()
|
|
assert data["status"] == "FAIL"
|
|
assert data["error_code"] == "VAL-422"
|
|
assert "errors" in data["data"]
|
|
|
|
def test_handles_validation_error_with_non_standard_loc(self):
|
|
"""Validation error with empty loc tuple maps the field to 'root'."""
|
|
from fastapi.exceptions import RequestValidationError
|
|
|
|
app = FastAPI()
|
|
init_exceptions_handlers(app)
|
|
|
|
@app.get("/root-error")
|
|
async def root_error():
|
|
raise RequestValidationError(
|
|
[
|
|
{
|
|
"type": "custom",
|
|
"loc": (),
|
|
"msg": "root level error",
|
|
"input": None,
|
|
"url": "",
|
|
}
|
|
]
|
|
)
|
|
|
|
client = TestClient(app)
|
|
response = client.get("/root-error")
|
|
|
|
assert response.status_code == 422
|
|
data = response.json()
|
|
assert data["data"]["errors"][0]["field"] == "root"
|
|
|
|
def test_openapi_schema_cached_after_first_call(self):
|
|
"""app.openapi() returns the cached schema on subsequent calls."""
|
|
from pydantic import BaseModel
|
|
|
|
app = FastAPI()
|
|
init_exceptions_handlers(app)
|
|
|
|
class Item(BaseModel):
|
|
name: str
|
|
|
|
@app.post("/items")
|
|
async def create_item(item: Item):
|
|
return item
|
|
|
|
schema_first = app.openapi()
|
|
schema_second = app.openapi()
|
|
assert schema_first is schema_second
|
|
|
|
def test_openapi_skips_operations_without_422(self):
|
|
"""_patched_openapi leaves operations that have no 422 response unchanged."""
|
|
app = FastAPI()
|
|
init_exceptions_handlers(app)
|
|
|
|
@app.get("/ping")
|
|
async def ping():
|
|
return {"ok": True}
|
|
|
|
schema = app.openapi()
|
|
get_op = schema["paths"]["/ping"]["get"]
|
|
assert "422" not in get_op["responses"]
|
|
assert "200" in get_op["responses"]
|
|
|
|
def test_openapi_skips_non_dict_path_item_values(self):
|
|
"""_patched_openapi ignores non-dict values in path items (e.g. path-level parameters)."""
|
|
from fastapi_toolsets.exceptions.handler import _patched_openapi
|
|
|
|
app = FastAPI()
|
|
|
|
def fake_openapi() -> dict:
|
|
return {
|
|
"paths": {
|
|
"/items": {
|
|
"parameters": [
|
|
{"name": "q", "in": "query"}
|
|
], # list, not a dict
|
|
"get": {"responses": {"200": {"description": "OK"}}},
|
|
}
|
|
}
|
|
}
|
|
|
|
schema = _patched_openapi(app, fake_openapi)
|
|
# The list value was skipped without error; the GET operation is intact
|
|
assert schema["paths"]["/items"]["parameters"] == [{"name": "q", "in": "query"}]
|
|
assert "422" not in schema["paths"]["/items"]["get"]["responses"]
|
|
|
|
|
|
class TestExceptionIntegration:
|
|
"""Integration tests for exception handling."""
|
|
|
|
@pytest.fixture
|
|
def app_with_routes(self):
|
|
"""Create app with test routes."""
|
|
app = FastAPI()
|
|
init_exceptions_handlers(app)
|
|
|
|
@app.get("/users/{user_id}")
|
|
async def get_user(user_id: int):
|
|
if user_id == 404:
|
|
raise NotFoundError()
|
|
if user_id == 401:
|
|
raise UnauthorizedError()
|
|
if user_id == 403:
|
|
raise ForbiddenError()
|
|
if user_id == 409:
|
|
raise ConflictError()
|
|
return {"id": user_id}
|
|
|
|
return app
|
|
|
|
def test_not_found_response(self, app_with_routes):
|
|
"""NotFoundError returns 404."""
|
|
client = TestClient(app_with_routes)
|
|
response = client.get("/users/404")
|
|
|
|
assert response.status_code == 404
|
|
assert response.json()["error_code"] == "RES-404"
|
|
|
|
def test_unauthorized_response(self, app_with_routes):
|
|
"""UnauthorizedError returns 401."""
|
|
client = TestClient(app_with_routes)
|
|
response = client.get("/users/401")
|
|
|
|
assert response.status_code == 401
|
|
assert response.json()["error_code"] == "AUTH-401"
|
|
|
|
def test_forbidden_response(self, app_with_routes):
|
|
"""ForbiddenError returns 403."""
|
|
client = TestClient(app_with_routes)
|
|
response = client.get("/users/403")
|
|
|
|
assert response.status_code == 403
|
|
assert response.json()["error_code"] == "AUTH-403"
|
|
|
|
def test_conflict_response(self, app_with_routes):
|
|
"""ConflictError returns 409."""
|
|
client = TestClient(app_with_routes)
|
|
response = client.get("/users/409")
|
|
|
|
assert response.status_code == 409
|
|
assert response.json()["error_code"] == "RES-409"
|
|
|
|
def test_success_response(self, app_with_routes):
|
|
"""Successful requests return normally."""
|
|
client = TestClient(app_with_routes)
|
|
response = client.get("/users/1")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {"id": 1}
|
|
|
|
|
|
class TestInvalidOrderFieldError:
|
|
"""Tests for InvalidOrderFieldError exception."""
|
|
|
|
def test_api_error_attributes(self):
|
|
"""InvalidOrderFieldError has correct api_error metadata."""
|
|
assert InvalidOrderFieldError.api_error.code == 422
|
|
assert InvalidOrderFieldError.api_error.err_code == "SORT-422"
|
|
assert InvalidOrderFieldError.api_error.msg == "Invalid Order Field"
|
|
|
|
def test_stores_field_and_valid_fields(self):
|
|
"""InvalidOrderFieldError stores field and valid_fields on the instance."""
|
|
error = InvalidOrderFieldError("unknown", ["name", "created_at"])
|
|
assert error.field == "unknown"
|
|
assert error.valid_fields == ["name", "created_at"]
|
|
|
|
def test_description_contains_field_and_valid_fields(self):
|
|
"""api_error.desc mentions the bad field and valid options."""
|
|
error = InvalidOrderFieldError("bad_field", ["name", "email"])
|
|
assert "bad_field" in error.api_error.desc
|
|
assert "name" in error.api_error.desc
|
|
assert "email" in error.api_error.desc
|
|
|
|
def test_handled_as_422_by_exception_handler(self):
|
|
"""init_exceptions_handlers turns InvalidOrderFieldError into a 422 response."""
|
|
app = FastAPI()
|
|
init_exceptions_handlers(app)
|
|
|
|
@app.get("/items")
|
|
async def list_items():
|
|
raise InvalidOrderFieldError("bad", ["name"])
|
|
|
|
client = TestClient(app)
|
|
response = client.get("/items")
|
|
|
|
assert response.status_code == 422
|
|
data = response.json()
|
|
assert data["error_code"] == "SORT-422"
|
|
assert data["status"] == "FAIL"
|