fastapi-mongo-base 1.3.4__tar.gz → 1.3.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {fastapi_mongo_base-1.3.4/src/fastapi_mongo_base.egg-info → fastapi_mongo_base-1.3.6}/PKG-INFO +1 -1
  2. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/pyproject.toml +1 -1
  3. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/app_factory.py +7 -1
  4. fastapi_mongo_base-1.3.6/src/fastapi_mongo_base/core/error_responses.py +137 -0
  5. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/exceptions.py +71 -30
  6. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/routes.py +8 -3
  7. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6/src/fastapi_mongo_base.egg-info}/PKG-INFO +1 -1
  8. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base.egg-info/SOURCES.txt +2 -0
  9. fastapi_mongo_base-1.3.6/tests/test_openapi_errors.py +116 -0
  10. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/LICENSE.txt +0 -0
  11. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/MANIFEST.in +0 -0
  12. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/README.md +0 -0
  13. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/boilerplate.md +0 -0
  14. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/changelog.md +0 -0
  15. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/contributing.md +0 -0
  16. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/endpoints.md +0 -0
  17. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/extra.css +0 -0
  18. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/index.md +0 -0
  19. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/installation.md +0 -0
  20. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/license.md +0 -0
  21. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/quickstart.md +0 -0
  22. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/settings.md +0 -0
  23. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/tutorial.md +0 -0
  24. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/docs/usage.md +0 -0
  25. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/mkdocs.yml +0 -0
  26. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/pytest.ini +0 -0
  27. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/setup.cfg +0 -0
  28. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/__init__.py +0 -0
  29. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/__init__.py +0 -0
  30. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/config.py +0 -0
  31. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/db.py +0 -0
  32. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/enums.py +0 -0
  33. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/errors/mongodb_errors.py +0 -0
  34. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/core/errors/resource_errors.py +0 -0
  35. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/models.py +0 -0
  36. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/py.typed +0 -0
  37. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/schemas.py +0 -0
  38. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/sql.py +0 -0
  39. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/tasks.py +0 -0
  40. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/utils/__init__.py +0 -0
  41. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/utils/basic.py +0 -0
  42. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/utils/bsontools.py +0 -0
  43. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/utils/timezone.py +0 -0
  44. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base/utils/usso_routes.py +0 -0
  45. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base.egg-info/dependency_links.txt +0 -0
  46. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base.egg-info/requires.txt +0 -0
  47. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/src/fastapi_mongo_base.egg-info/top_level.txt +0 -0
  48. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/__init__.py +0 -0
  49. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/app/__init__.py +0 -0
  50. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/app/logs/app.log +0 -0
  51. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/app/main.py +0 -0
  52. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/app/server.py +0 -0
  53. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/conftest.py +0 -0
  54. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/test_apis.py +0 -0
  55. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/test_app.py +0 -0
  56. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/test_mongodb_errors.py +0 -0
  57. {fastapi_mongo_base-1.3.4 → fastapi_mongo_base-1.3.6}/tests/test_resource_errors.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-mongo-base
3
- Version: 1.3.4
3
+ Version: 1.3.6
4
4
  Summary: A simple boilerplate application, including models and schemas and abstract router, for FastAPI with MongoDB
5
5
  Author-email: Mahdi Kiani <mahdikiany@gmail.com>
6
6
  Maintainer-email: Mahdi Kiani <mahdikiany@gmail.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-mongo-base"
7
- version = "1.3.4"
7
+ version = "1.3.6"
8
8
  description = "A simple boilerplate application, including models and schemas and abstract router, for FastAPI with MongoDB"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -12,6 +12,7 @@ from fastapi.responses import RedirectResponse
12
12
  from fastapi.staticfiles import StaticFiles
13
13
 
14
14
  from . import config, db, exceptions
15
+ from .error_responses import COMMON_ERROR_RESPONSES, setup_openapi_errors
15
16
 
16
17
 
17
18
  def health(request: fastapi.Request) -> dict[str, str]:
@@ -258,7 +259,10 @@ def create_app(
258
259
  license_info=license_info,
259
260
  )
260
261
 
261
- app = fastapi.FastAPI(**data)
262
+ app = fastapi.FastAPI(
263
+ **data,
264
+ responses=kwargs.pop("responses", COMMON_ERROR_RESPONSES),
265
+ )
262
266
 
263
267
  app = configure_app(
264
268
  app=app,
@@ -356,4 +360,6 @@ def configure_app(
356
360
  name="coverage",
357
361
  )
358
362
 
363
+ setup_openapi_errors(app)
364
+
359
365
  return app
@@ -0,0 +1,137 @@
1
+ """OpenAPI error response schemas aligned with exception handlers."""
2
+
3
+ from typing import Any
4
+
5
+ import fastapi
6
+ from fastapi.openapi.utils import get_openapi
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+
10
+ class APIErrorResponseModel(BaseModel):
11
+ """Structured error response for BaseHTTPException handlers."""
12
+
13
+ model_config = ConfigDict(extra="allow")
14
+
15
+ message: dict[str, str]
16
+ error_code: str
17
+ detail: str | None = None
18
+
19
+
20
+ class ValidationReason(BaseModel):
21
+ """Single field validation failure."""
22
+
23
+ model_config = ConfigDict(extra="allow")
24
+
25
+ field: str
26
+ type: str | None = None
27
+ msg: str | None = None
28
+ input: Any = None
29
+ ctx: dict[str, Any] | None = None
30
+
31
+
32
+ class ValidationErrorResponseModel(BaseModel):
33
+ """Structured validation error response."""
34
+
35
+ message: dict[str, str]
36
+ error_code: str = "validation_error"
37
+ detail: None = None
38
+ reasons: list[ValidationReason] = Field(default_factory=list)
39
+
40
+
41
+ class InternalErrorResponseModel(BaseModel):
42
+ """Fallback response for unhandled exceptions."""
43
+
44
+ message: str
45
+ error: str = "Exception"
46
+
47
+
48
+ COMMON_ERROR_RESPONSES: dict[int, dict[str, Any]] = {
49
+ 400: {
50
+ "model": APIErrorResponseModel,
51
+ "description": "Bad request",
52
+ },
53
+ 401: {
54
+ "model": APIErrorResponseModel,
55
+ "description": "Unauthorized",
56
+ },
57
+ 402: {
58
+ "model": APIErrorResponseModel,
59
+ "description": "Payment required",
60
+ },
61
+ 403: {
62
+ "model": APIErrorResponseModel,
63
+ "description": "Forbidden",
64
+ },
65
+ 404: {
66
+ "model": APIErrorResponseModel,
67
+ "description": "Not found",
68
+ },
69
+ 409: {
70
+ "model": APIErrorResponseModel,
71
+ "description": "Conflict",
72
+ },
73
+ 410: {
74
+ "model": APIErrorResponseModel,
75
+ "description": "Gone",
76
+ },
77
+ 422: {
78
+ "model": ValidationErrorResponseModel,
79
+ "description": "Validation error",
80
+ },
81
+ 423: {
82
+ "model": APIErrorResponseModel,
83
+ "description": "Locked",
84
+ },
85
+ 500: {
86
+ "model": APIErrorResponseModel,
87
+ "description": "Internal server error",
88
+ },
89
+ }
90
+
91
+
92
+ def _patch_validation_responses(schema: dict[str, Any]) -> None:
93
+ validation_ref = {
94
+ "$ref": "#/components/schemas/ValidationErrorResponseModel"
95
+ }
96
+ for path_item in schema.get("paths", {}).values():
97
+ for operation in path_item.values():
98
+ if not isinstance(operation, dict):
99
+ continue
100
+ responses = operation.get("responses")
101
+ if not isinstance(responses, dict) or "422" not in responses:
102
+ continue
103
+ responses["422"] = {
104
+ "description": "Validation error",
105
+ "content": {
106
+ "application/json": {
107
+ "schema": validation_ref,
108
+ },
109
+ },
110
+ }
111
+
112
+
113
+ def setup_openapi_errors(app: fastapi.FastAPI) -> None:
114
+ """
115
+ Replace default FastAPI validation error schemas in OpenAPI.
116
+
117
+ Args:
118
+ app: FastAPI application instance.
119
+
120
+ """
121
+
122
+ def custom_openapi() -> dict[str, Any]:
123
+ if app.openapi_schema:
124
+ return app.openapi_schema
125
+
126
+ schema = get_openapi(
127
+ title=app.title,
128
+ version=app.version,
129
+ openapi_version=app.openapi_version,
130
+ description=app.description,
131
+ routes=app.routes,
132
+ )
133
+ _patch_validation_responses(schema)
134
+ app.openapi_schema = schema
135
+ return app.openapi_schema
136
+
137
+ app.openapi = custom_openapi
@@ -15,6 +15,13 @@ from fastapi.exceptions import (
15
15
  from fastapi.responses import JSONResponse
16
16
  from pydantic import ValidationError
17
17
 
18
+ from fastapi_mongo_base.core.error_responses import (
19
+ APIErrorResponseModel,
20
+ InternalErrorResponseModel,
21
+ ValidationErrorResponseModel,
22
+ ValidationReason,
23
+ )
24
+
18
25
  try:
19
26
  from usso.integrations.fastapi import (
20
27
  EXCEPTION_HANDLERS as usso_exception_handler, # noqa: N811
@@ -100,45 +107,85 @@ def base_http_exception_handler(
100
107
  logging.debug("base_http_exception_handler: %s\n%s", request.url, exc)
101
108
 
102
109
  if request.headers.get("accept-language"):
103
- locale = request.headers.get("accept-language").split(",")[0]
104
- message = {locale: exc.message.get(locale)}
110
+ locales = request.headers.get("accept-language").split(",")
111
+ msg = {}
112
+ for locale in locales:
113
+ lang = locale.split("-")[0]
114
+ if lang in exc.message:
115
+ msg[lang] = exc.message.get(lang)
116
+ message = msg
105
117
  else:
106
118
  message = exc.message
107
119
 
108
- return JSONResponse(
109
- status_code=exc.status_code,
110
- content={
111
- "message": message,
112
- "error": exc.error_code,
113
- "detail": exc.detail,
114
- **exc.data,
115
- },
116
- )
120
+ content = APIErrorResponseModel(
121
+ message=message,
122
+ error_code=exc.error_code,
123
+ detail=exc.detail,
124
+ **exc.data,
125
+ ).model_dump()
126
+ return JSONResponse(status_code=exc.status_code, content=content)
127
+
128
+
129
+ def _resolve_validation_message(request: Request) -> dict[str, str]:
130
+ message = {
131
+ "en": "Validation error",
132
+ "fa": "اطلاعات وارد شده صحیح نیست",
133
+ }
134
+ if request.headers.get("accept-language"):
135
+ locales = request.headers.get("accept-language").split(",")
136
+ filtered_message = {}
137
+ for locale in locales:
138
+ lang = locale.split("-")[0]
139
+ if lang in message:
140
+ filtered_message[lang] = message[lang]
141
+ return filtered_message or message
142
+ return message
143
+
144
+
145
+ def _format_validation_reasons(errors: list[dict]) -> list[dict]:
146
+ reasons_by_field: dict[str, dict] = {}
147
+ for error in json.loads(json_advanced.dumps(errors)):
148
+ loc = error.pop("loc", ())
149
+ field = str(loc[-1]) if loc else "unknown"
150
+ error.pop("url", None)
151
+ error["field"] = field
152
+ reasons_by_field.setdefault(field, error)
153
+ return list(reasons_by_field.values())
154
+
155
+
156
+ def _validation_error_response(
157
+ request: Request,
158
+ errors: list[dict],
159
+ status_code: int = 422,
160
+ ) -> JSONResponse:
161
+ content = ValidationErrorResponseModel(
162
+ message=_resolve_validation_message(request),
163
+ reasons=[
164
+ ValidationReason(**reason)
165
+ for reason in _format_validation_reasons(errors)
166
+ ],
167
+ ).model_dump()
168
+ return JSONResponse(status_code=status_code, content=content)
117
169
 
118
170
 
119
171
  def pydantic_exception_handler(
120
- request: Request, exc: ValidationError
172
+ request: Request, exc: RequestValidationError | ResponseValidationError
121
173
  ) -> JSONResponse:
122
174
  """
123
175
  Handle Pydantic validation errors and return JSON response.
124
176
 
125
177
  Args:
126
178
  request: FastAPI request object.
127
- exc: ValidationError instance.
179
+ exc: RequestValidationError or ResponseValidationError instance.
128
180
 
129
181
  Returns:
130
182
  JSONResponse with validation error details.
131
183
 
132
184
  """
133
185
  logging.debug("pydantic_exception_handler: %s\n%s", request.url, exc)
134
- return JSONResponse(
135
- status_code=500,
136
- content={
137
- "message": str(exc),
138
- "error": "Exception",
139
- "errors": json.loads(json_advanced.dumps(exc.errors())),
140
- },
141
- )
186
+
187
+ status_code = 500 if isinstance(exc, ResponseValidationError) else 422
188
+ return _validation_error_response(request, exc.errors(), status_code)
142
189
 
143
190
 
144
191
  async def request_validation_exception_handler(
@@ -180,11 +227,7 @@ async def request_validation_exception_handler(
180
227
  dict(request.headers),
181
228
  )
182
229
 
183
- from fastapi.exception_handlers import (
184
- request_validation_exception_handler as default_handler,
185
- )
186
-
187
- return await default_handler(request, exc)
230
+ return _validation_error_response(request, exc.errors())
188
231
 
189
232
 
190
233
  def mongodb_exception_handler(
@@ -248,10 +291,8 @@ def general_exception_handler(
248
291
  except ImportError:
249
292
  pass
250
293
 
251
- return JSONResponse(
252
- status_code=500,
253
- content={"message": str(exc), "error": "Exception"},
254
- )
294
+ content = InternalErrorResponseModel(message=str(exc)).model_dump()
295
+ return JSONResponse(status_code=500, content=content)
255
296
 
256
297
 
257
298
  # A dictionary for dynamic registration
@@ -11,6 +11,7 @@ from fastapi import APIRouter, BackgroundTasks, Query, Request
11
11
  from pydantic import BaseModel
12
12
 
13
13
  from .core.config import Settings
14
+ from .core.error_responses import COMMON_ERROR_RESPONSES
14
15
  from .core.exceptions import BaseHTTPException
15
16
  from .models import BaseEntity
16
17
  from .schemas import BaseEntitySchema, PaginatedResponse
@@ -90,9 +91,13 @@ class AbstractBaseRouter(metaclass=singleton.Singleton):
90
91
  if tags is None:
91
92
  tags = [self.model.__name__]
92
93
 
93
- self.router = APIRouter(
94
- prefix=prefix, tags=cast(list[str | Enum], tags), **kwargs
95
- )
94
+ router_kwargs = {
95
+ "prefix": prefix,
96
+ "tags": cast(list[str | Enum], tags),
97
+ "responses": kwargs.pop("responses", COMMON_ERROR_RESPONSES),
98
+ **kwargs,
99
+ }
100
+ self.router = APIRouter(**router_kwargs)
96
101
 
97
102
  self.config_schemas(self.schema, **kwargs)
98
103
  self.config_routes(**kwargs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-mongo-base
3
- Version: 1.3.4
3
+ Version: 1.3.6
4
4
  Summary: A simple boilerplate application, including models and schemas and abstract router, for FastAPI with MongoDB
5
5
  Author-email: Mahdi Kiani <mahdikiany@gmail.com>
6
6
  Maintainer-email: Mahdi Kiani <mahdikiany@gmail.com>
@@ -33,6 +33,7 @@ src/fastapi_mongo_base/core/app_factory.py
33
33
  src/fastapi_mongo_base/core/config.py
34
34
  src/fastapi_mongo_base/core/db.py
35
35
  src/fastapi_mongo_base/core/enums.py
36
+ src/fastapi_mongo_base/core/error_responses.py
36
37
  src/fastapi_mongo_base/core/exceptions.py
37
38
  src/fastapi_mongo_base/core/errors/mongodb_errors.py
38
39
  src/fastapi_mongo_base/core/errors/resource_errors.py
@@ -46,6 +47,7 @@ tests/conftest.py
46
47
  tests/test_apis.py
47
48
  tests/test_app.py
48
49
  tests/test_mongodb_errors.py
50
+ tests/test_openapi_errors.py
49
51
  tests/test_resource_errors.py
50
52
  tests/app/__init__.py
51
53
  tests/app/main.py
@@ -0,0 +1,116 @@
1
+ """Tests for OpenAPI error response schemas."""
2
+
3
+ from fastapi.testclient import TestClient
4
+
5
+ from fastapi_mongo_base.core.error_responses import (
6
+ APIErrorResponseModel,
7
+ ValidationErrorResponseModel,
8
+ )
9
+ from fastapi_mongo_base.core.exceptions import BaseHTTPException
10
+
11
+ from .app.server import app as fastapi_app
12
+
13
+
14
+ def test_openapi_documents_error_schemas() -> None:
15
+ """OpenAPI should document custom error response schemas."""
16
+ schema = fastapi_app.openapi()
17
+
18
+ assert "APIErrorResponseModel" in schema["components"]["schemas"]
19
+ assert "ValidationErrorResponseModel" in schema["components"]["schemas"]
20
+
21
+ list_get = schema["paths"]["/test"]["get"]["responses"]
22
+ assert "404" in list_get
23
+ assert (
24
+ list_get["404"]["content"]["application/json"]["schema"]["$ref"]
25
+ == "#/components/schemas/APIErrorResponseModel"
26
+ )
27
+
28
+ create_post = schema["paths"]["/test"]["post"]["responses"]
29
+ assert "422" in create_post
30
+ assert (
31
+ create_post["422"]["content"]["application/json"]["schema"]["$ref"]
32
+ == "#/components/schemas/ValidationErrorResponseModel"
33
+ )
34
+
35
+
36
+ def test_openapi_error_models_match_handler_output() -> None:
37
+ """Documented schemas should include fields returned by handlers."""
38
+ api_schema = APIErrorResponseModel.model_json_schema()
39
+ assert "message" in api_schema["properties"]
40
+ assert "error_code" in api_schema["properties"]
41
+ assert "detail" in api_schema["properties"]
42
+
43
+ validation_schema = ValidationErrorResponseModel.model_json_schema()
44
+ assert "reasons" in validation_schema["properties"]
45
+ assert validation_schema["properties"]["error_code"]["default"] == (
46
+ "validation_error"
47
+ )
48
+
49
+
50
+ def test_runtime_validation_error_matches_openapi_schema() -> None:
51
+ """Validation handler response should match documented error shape."""
52
+ client = TestClient(fastapi_app)
53
+ response = client.post("/test", json={})
54
+
55
+ assert response.status_code == 422
56
+ body = response.json()
57
+ assert body["error_code"] == "validation_error"
58
+ assert "message" in body
59
+ assert "reasons" in body
60
+ assert isinstance(body["reasons"], list)
61
+
62
+
63
+ def test_runtime_base_http_exception_matches_openapi_schema() -> None:
64
+ """BaseHTTPException handler response should match documented shape."""
65
+ from starlette.requests import Request
66
+
67
+ from fastapi_mongo_base.core.exceptions import base_http_exception_handler
68
+
69
+ request = Request({
70
+ "type": "http",
71
+ "headers": [],
72
+ "method": "GET",
73
+ "path": "/test/not-a-real-uid",
74
+ })
75
+ exc = BaseHTTPException(
76
+ status_code=404,
77
+ error_code="item_not_found",
78
+ message={"en": "Testentity not found"},
79
+ )
80
+
81
+ response = base_http_exception_handler(request, exc)
82
+ body = response.body.decode()
83
+
84
+ assert response.status_code == 404
85
+ assert "item_not_found" in body
86
+ assert "Testentity not found" in body
87
+
88
+
89
+ def test_custom_route_documents_api_error_response() -> None:
90
+ """App-level responses should document APIErrorResponseModel on routes."""
91
+ from fastapi import FastAPI
92
+
93
+ from fastapi_mongo_base.core.app_factory import setup_exception_handlers
94
+ from fastapi_mongo_base.core.error_responses import (
95
+ COMMON_ERROR_RESPONSES,
96
+ setup_openapi_errors,
97
+ )
98
+
99
+ app = FastAPI(responses=COMMON_ERROR_RESPONSES)
100
+ setup_exception_handlers(app=app)
101
+ setup_openapi_errors(app)
102
+
103
+ @app.get("/missing")
104
+ def raise_not_found() -> None:
105
+ raise BaseHTTPException(
106
+ status_code=404,
107
+ error_code="item_not_found",
108
+ message={"en": "Item not found"},
109
+ )
110
+
111
+ schema = app.openapi()
112
+ responses = schema["paths"]["/missing"]["get"]["responses"]
113
+ assert (
114
+ responses["404"]["content"]["application/json"]["schema"]["$ref"]
115
+ == "#/components/schemas/APIErrorResponseModel"
116
+ )