fastapi-errors-plus 0.6.1__tar.gz → 0.6.2__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.
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/LICENSE +1 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/PKG-INFO +97 -1
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/README.md +96 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus/__init__.py +1 -1
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus/base.py +1 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus/errors.py +3 -3
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus/protocol.py +1 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus.egg-info/PKG-INFO +97 -1
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/pyproject.toml +15 -1
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/tests/test_app.py +4 -55
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/tests/test_base.py +5 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/tests/test_errors.py +114 -15
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/tests/test_integration.py +21 -7
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus/py.typed +0 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus.egg-info/SOURCES.txt +0 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus.egg-info/dependency_links.txt +0 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus.egg-info/requires.txt +0 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus.egg-info/top_level.txt +0 -0
- {fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-errors-plus
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Universal library for documenting errors in FastAPI endpoints
|
|
5
5
|
Author-email: Igor Selivanov <seligoroff@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -335,6 +335,102 @@ Any class implementing this protocol (through structural typing) can be used wit
|
|
|
335
335
|
|
|
336
336
|
Both approaches work together — you can mix them in the same `Errors()` call!
|
|
337
337
|
|
|
338
|
+
## Using Pydantic with ErrorDTO
|
|
339
|
+
|
|
340
|
+
**Note:** Pydantic is **not required** to use this library. This section is for projects that already use Pydantic and want to integrate it with the ErrorDTO protocol.
|
|
341
|
+
|
|
342
|
+
Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `to_example()`) will work, including Pydantic models.
|
|
343
|
+
|
|
344
|
+
### Simple Pydantic Model as ErrorDTO
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
from pydantic import BaseModel, Field
|
|
348
|
+
from fastapi_errors_plus import Errors
|
|
349
|
+
from typing import Dict, Any
|
|
350
|
+
|
|
351
|
+
class PydanticErrorDTO(BaseModel):
|
|
352
|
+
"""Pydantic model implementing ErrorDTO Protocol."""
|
|
353
|
+
status_code: int = Field(..., ge=400, le=599, description="HTTP status code")
|
|
354
|
+
message: str = Field(..., min_length=1, description="Error message")
|
|
355
|
+
|
|
356
|
+
def to_example(self) -> Dict[str, Any]:
|
|
357
|
+
"""Generate example for OpenAPI."""
|
|
358
|
+
return {
|
|
359
|
+
self.message: {
|
|
360
|
+
"value": {"detail": self.message},
|
|
361
|
+
},
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
# Usage
|
|
365
|
+
notification_error = PydanticErrorDTO(
|
|
366
|
+
status_code=404,
|
|
367
|
+
message="Notification not found",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
@router.delete(
|
|
371
|
+
"/{id}",
|
|
372
|
+
responses=Errors(notification_error),
|
|
373
|
+
)
|
|
374
|
+
def delete_item(id: int):
|
|
375
|
+
pass
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Benefits:**
|
|
379
|
+
- Runtime validation through Pydantic
|
|
380
|
+
- Type safety
|
|
381
|
+
- Automatic field documentation
|
|
382
|
+
- Works with ErrorDTO Protocol through structural typing
|
|
383
|
+
|
|
384
|
+
### Complex ErrorDTO with Pydantic
|
|
385
|
+
|
|
386
|
+
For errors with additional fields:
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
from pydantic import BaseModel, Field
|
|
390
|
+
from fastapi_errors_plus import Errors
|
|
391
|
+
from typing import Dict, Any, Optional
|
|
392
|
+
|
|
393
|
+
class DetailedErrorDTO(BaseModel):
|
|
394
|
+
"""Pydantic model for errors with additional fields."""
|
|
395
|
+
status_code: int = Field(..., ge=400, le=599)
|
|
396
|
+
message: str = Field(..., min_length=1)
|
|
397
|
+
error_code: Optional[str] = Field(None, description="Internal error code")
|
|
398
|
+
timestamp: Optional[str] = Field(None, description="Error timestamp")
|
|
399
|
+
|
|
400
|
+
def to_example(self) -> Dict[str, Any]:
|
|
401
|
+
"""Generate example for OpenAPI."""
|
|
402
|
+
example = {"detail": self.message}
|
|
403
|
+
if self.error_code:
|
|
404
|
+
example["error_code"] = self.error_code
|
|
405
|
+
if self.timestamp:
|
|
406
|
+
example["timestamp"] = self.timestamp
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
self.message: {
|
|
410
|
+
"value": example,
|
|
411
|
+
},
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
# Usage
|
|
415
|
+
validation_error = DetailedErrorDTO(
|
|
416
|
+
status_code=422,
|
|
417
|
+
message="Validation failed",
|
|
418
|
+
error_code="VALIDATION_ERROR",
|
|
419
|
+
timestamp="2025-01-15T10:30:00Z",
|
|
420
|
+
)
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**When to use Pydantic with ErrorDTO:**
|
|
424
|
+
- Your project already uses Pydantic extensively
|
|
425
|
+
- You need runtime validation for error objects
|
|
426
|
+
- You want automatic field documentation
|
|
427
|
+
- You have complex error structures with multiple fields
|
|
428
|
+
|
|
429
|
+
**When not to use Pydantic:**
|
|
430
|
+
- Your project doesn't use Pydantic (use `BaseErrorDTO` or `StandardErrorDTO` instead)
|
|
431
|
+
- You need simple error objects (dataclasses are sufficient)
|
|
432
|
+
- You want to keep dependencies minimal
|
|
433
|
+
|
|
338
434
|
## Best Practice: Connecting Exceptions and ErrorDTO
|
|
339
435
|
|
|
340
436
|
### Problem
|
|
@@ -309,6 +309,102 @@ Any class implementing this protocol (through structural typing) can be used wit
|
|
|
309
309
|
|
|
310
310
|
Both approaches work together — you can mix them in the same `Errors()` call!
|
|
311
311
|
|
|
312
|
+
## Using Pydantic with ErrorDTO
|
|
313
|
+
|
|
314
|
+
**Note:** Pydantic is **not required** to use this library. This section is for projects that already use Pydantic and want to integrate it with the ErrorDTO protocol.
|
|
315
|
+
|
|
316
|
+
Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `to_example()`) will work, including Pydantic models.
|
|
317
|
+
|
|
318
|
+
### Simple Pydantic Model as ErrorDTO
|
|
319
|
+
|
|
320
|
+
```python
|
|
321
|
+
from pydantic import BaseModel, Field
|
|
322
|
+
from fastapi_errors_plus import Errors
|
|
323
|
+
from typing import Dict, Any
|
|
324
|
+
|
|
325
|
+
class PydanticErrorDTO(BaseModel):
|
|
326
|
+
"""Pydantic model implementing ErrorDTO Protocol."""
|
|
327
|
+
status_code: int = Field(..., ge=400, le=599, description="HTTP status code")
|
|
328
|
+
message: str = Field(..., min_length=1, description="Error message")
|
|
329
|
+
|
|
330
|
+
def to_example(self) -> Dict[str, Any]:
|
|
331
|
+
"""Generate example for OpenAPI."""
|
|
332
|
+
return {
|
|
333
|
+
self.message: {
|
|
334
|
+
"value": {"detail": self.message},
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# Usage
|
|
339
|
+
notification_error = PydanticErrorDTO(
|
|
340
|
+
status_code=404,
|
|
341
|
+
message="Notification not found",
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
@router.delete(
|
|
345
|
+
"/{id}",
|
|
346
|
+
responses=Errors(notification_error),
|
|
347
|
+
)
|
|
348
|
+
def delete_item(id: int):
|
|
349
|
+
pass
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Benefits:**
|
|
353
|
+
- Runtime validation through Pydantic
|
|
354
|
+
- Type safety
|
|
355
|
+
- Automatic field documentation
|
|
356
|
+
- Works with ErrorDTO Protocol through structural typing
|
|
357
|
+
|
|
358
|
+
### Complex ErrorDTO with Pydantic
|
|
359
|
+
|
|
360
|
+
For errors with additional fields:
|
|
361
|
+
|
|
362
|
+
```python
|
|
363
|
+
from pydantic import BaseModel, Field
|
|
364
|
+
from fastapi_errors_plus import Errors
|
|
365
|
+
from typing import Dict, Any, Optional
|
|
366
|
+
|
|
367
|
+
class DetailedErrorDTO(BaseModel):
|
|
368
|
+
"""Pydantic model for errors with additional fields."""
|
|
369
|
+
status_code: int = Field(..., ge=400, le=599)
|
|
370
|
+
message: str = Field(..., min_length=1)
|
|
371
|
+
error_code: Optional[str] = Field(None, description="Internal error code")
|
|
372
|
+
timestamp: Optional[str] = Field(None, description="Error timestamp")
|
|
373
|
+
|
|
374
|
+
def to_example(self) -> Dict[str, Any]:
|
|
375
|
+
"""Generate example for OpenAPI."""
|
|
376
|
+
example = {"detail": self.message}
|
|
377
|
+
if self.error_code:
|
|
378
|
+
example["error_code"] = self.error_code
|
|
379
|
+
if self.timestamp:
|
|
380
|
+
example["timestamp"] = self.timestamp
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
self.message: {
|
|
384
|
+
"value": example,
|
|
385
|
+
},
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# Usage
|
|
389
|
+
validation_error = DetailedErrorDTO(
|
|
390
|
+
status_code=422,
|
|
391
|
+
message="Validation failed",
|
|
392
|
+
error_code="VALIDATION_ERROR",
|
|
393
|
+
timestamp="2025-01-15T10:30:00Z",
|
|
394
|
+
)
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**When to use Pydantic with ErrorDTO:**
|
|
398
|
+
- Your project already uses Pydantic extensively
|
|
399
|
+
- You need runtime validation for error objects
|
|
400
|
+
- You want automatic field documentation
|
|
401
|
+
- You have complex error structures with multiple fields
|
|
402
|
+
|
|
403
|
+
**When not to use Pydantic:**
|
|
404
|
+
- Your project doesn't use Pydantic (use `BaseErrorDTO` or `StandardErrorDTO` instead)
|
|
405
|
+
- You need simple error objects (dataclasses are sufficient)
|
|
406
|
+
- You want to keep dependencies minimal
|
|
407
|
+
|
|
312
408
|
## Best Practice: Connecting Exceptions and ErrorDTO
|
|
313
409
|
|
|
314
410
|
### Problem
|
|
@@ -140,7 +140,7 @@ class Errors(Mapping):
|
|
|
140
140
|
|
|
141
141
|
if add_422:
|
|
142
142
|
self._add_standard_error(
|
|
143
|
-
status.
|
|
143
|
+
status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
144
144
|
"Validation Error",
|
|
145
145
|
{"detail": "Validation error"},
|
|
146
146
|
)
|
|
@@ -239,7 +239,7 @@ class Errors(Mapping):
|
|
|
239
239
|
STANDARD_DESCRIPTIONS = {
|
|
240
240
|
status.HTTP_401_UNAUTHORIZED: "Unauthorized",
|
|
241
241
|
status.HTTP_403_FORBIDDEN: "Forbidden",
|
|
242
|
-
status.
|
|
242
|
+
status.HTTP_422_UNPROCESSABLE_CONTENT: "Validation Error",
|
|
243
243
|
status.HTTP_500_INTERNAL_SERVER_ERROR: "Internal Server Error",
|
|
244
244
|
}
|
|
245
245
|
|
|
@@ -263,7 +263,7 @@ class Errors(Mapping):
|
|
|
263
263
|
standard_keys = {
|
|
264
264
|
status.HTTP_401_UNAUTHORIZED: "StandardUnauthorized",
|
|
265
265
|
status.HTTP_403_FORBIDDEN: "StandardForbidden",
|
|
266
|
-
status.
|
|
266
|
+
status.HTTP_422_UNPROCESSABLE_CONTENT: "StandardValidationError",
|
|
267
267
|
status.HTTP_500_INTERNAL_SERVER_ERROR: "StandardInternalServerError",
|
|
268
268
|
}
|
|
269
269
|
example_key = standard_keys.get(status_code, f"Standard{status_code}")
|
{fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-errors-plus
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: Universal library for documenting errors in FastAPI endpoints
|
|
5
5
|
Author-email: Igor Selivanov <seligoroff@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -335,6 +335,102 @@ Any class implementing this protocol (through structural typing) can be used wit
|
|
|
335
335
|
|
|
336
336
|
Both approaches work together — you can mix them in the same `Errors()` call!
|
|
337
337
|
|
|
338
|
+
## Using Pydantic with ErrorDTO
|
|
339
|
+
|
|
340
|
+
**Note:** Pydantic is **not required** to use this library. This section is for projects that already use Pydantic and want to integrate it with the ErrorDTO protocol.
|
|
341
|
+
|
|
342
|
+
Since the library uses structural typing (Protocol), any class that implements the required attributes (`status_code`, `message`, `to_example()`) will work, including Pydantic models.
|
|
343
|
+
|
|
344
|
+
### Simple Pydantic Model as ErrorDTO
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
from pydantic import BaseModel, Field
|
|
348
|
+
from fastapi_errors_plus import Errors
|
|
349
|
+
from typing import Dict, Any
|
|
350
|
+
|
|
351
|
+
class PydanticErrorDTO(BaseModel):
|
|
352
|
+
"""Pydantic model implementing ErrorDTO Protocol."""
|
|
353
|
+
status_code: int = Field(..., ge=400, le=599, description="HTTP status code")
|
|
354
|
+
message: str = Field(..., min_length=1, description="Error message")
|
|
355
|
+
|
|
356
|
+
def to_example(self) -> Dict[str, Any]:
|
|
357
|
+
"""Generate example for OpenAPI."""
|
|
358
|
+
return {
|
|
359
|
+
self.message: {
|
|
360
|
+
"value": {"detail": self.message},
|
|
361
|
+
},
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
# Usage
|
|
365
|
+
notification_error = PydanticErrorDTO(
|
|
366
|
+
status_code=404,
|
|
367
|
+
message="Notification not found",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
@router.delete(
|
|
371
|
+
"/{id}",
|
|
372
|
+
responses=Errors(notification_error),
|
|
373
|
+
)
|
|
374
|
+
def delete_item(id: int):
|
|
375
|
+
pass
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Benefits:**
|
|
379
|
+
- Runtime validation through Pydantic
|
|
380
|
+
- Type safety
|
|
381
|
+
- Automatic field documentation
|
|
382
|
+
- Works with ErrorDTO Protocol through structural typing
|
|
383
|
+
|
|
384
|
+
### Complex ErrorDTO with Pydantic
|
|
385
|
+
|
|
386
|
+
For errors with additional fields:
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
from pydantic import BaseModel, Field
|
|
390
|
+
from fastapi_errors_plus import Errors
|
|
391
|
+
from typing import Dict, Any, Optional
|
|
392
|
+
|
|
393
|
+
class DetailedErrorDTO(BaseModel):
|
|
394
|
+
"""Pydantic model for errors with additional fields."""
|
|
395
|
+
status_code: int = Field(..., ge=400, le=599)
|
|
396
|
+
message: str = Field(..., min_length=1)
|
|
397
|
+
error_code: Optional[str] = Field(None, description="Internal error code")
|
|
398
|
+
timestamp: Optional[str] = Field(None, description="Error timestamp")
|
|
399
|
+
|
|
400
|
+
def to_example(self) -> Dict[str, Any]:
|
|
401
|
+
"""Generate example for OpenAPI."""
|
|
402
|
+
example = {"detail": self.message}
|
|
403
|
+
if self.error_code:
|
|
404
|
+
example["error_code"] = self.error_code
|
|
405
|
+
if self.timestamp:
|
|
406
|
+
example["timestamp"] = self.timestamp
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
self.message: {
|
|
410
|
+
"value": example,
|
|
411
|
+
},
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
# Usage
|
|
415
|
+
validation_error = DetailedErrorDTO(
|
|
416
|
+
status_code=422,
|
|
417
|
+
message="Validation failed",
|
|
418
|
+
error_code="VALIDATION_ERROR",
|
|
419
|
+
timestamp="2025-01-15T10:30:00Z",
|
|
420
|
+
)
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**When to use Pydantic with ErrorDTO:**
|
|
424
|
+
- Your project already uses Pydantic extensively
|
|
425
|
+
- You need runtime validation for error objects
|
|
426
|
+
- You want automatic field documentation
|
|
427
|
+
- You have complex error structures with multiple fields
|
|
428
|
+
|
|
429
|
+
**When not to use Pydantic:**
|
|
430
|
+
- Your project doesn't use Pydantic (use `BaseErrorDTO` or `StandardErrorDTO` instead)
|
|
431
|
+
- You need simple error objects (dataclasses are sufficient)
|
|
432
|
+
- You want to keep dependencies minimal
|
|
433
|
+
|
|
338
434
|
## Best Practice: Connecting Exceptions and ErrorDTO
|
|
339
435
|
|
|
340
436
|
### Problem
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fastapi-errors-plus"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.2"
|
|
8
8
|
description = "Universal library for documenting errors in FastAPI endpoints"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -45,9 +45,23 @@ testpaths = ["tests"]
|
|
|
45
45
|
python_files = "test_*.py"
|
|
46
46
|
python_classes = "Test*"
|
|
47
47
|
python_functions = "test_*"
|
|
48
|
+
markers = [
|
|
49
|
+
"unit: Unit tests for individual components",
|
|
50
|
+
"integration: Integration tests with FastAPI application",
|
|
51
|
+
]
|
|
52
|
+
addopts = [
|
|
53
|
+
"--cov=fastapi_errors_plus",
|
|
54
|
+
"--cov-report=term-missing",
|
|
55
|
+
"--cov-report=html:tests/htmlcov",
|
|
56
|
+
"--cov-report=xml:tests/coverage.xml",
|
|
57
|
+
"--cov-branch",
|
|
58
|
+
"--cov-fail-under=80",
|
|
59
|
+
"-v",
|
|
60
|
+
]
|
|
48
61
|
|
|
49
62
|
[tool.coverage.run]
|
|
50
63
|
source = ["fastapi_errors_plus"]
|
|
64
|
+
data_file = "tests/.coverage"
|
|
51
65
|
omit = [
|
|
52
66
|
"*/tests/*",
|
|
53
67
|
"*/__pycache__/*",
|
|
@@ -33,7 +33,7 @@ class DomainException(Exception):
|
|
|
33
33
|
return cls()
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
class
|
|
36
|
+
class MockItemNotFoundError(DomainException):
|
|
37
37
|
"""Test item not found error."""
|
|
38
38
|
status_code = status.HTTP_404_NOT_FOUND
|
|
39
39
|
message = "Test item not found"
|
|
@@ -48,7 +48,7 @@ class TestItemNotFoundError(DomainException):
|
|
|
48
48
|
return cls(item_id="test_id")
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
class
|
|
51
|
+
class MockItemAccessDeniedError(DomainException):
|
|
52
52
|
"""Test item access denied error."""
|
|
53
53
|
status_code = status.HTTP_403_FORBIDDEN
|
|
54
54
|
message = "Test item access denied"
|
|
@@ -273,63 +273,12 @@ def create_item_mixed_base_dto(item_id: int):
|
|
|
273
273
|
return {"message": f"Item {item_id} created"}
|
|
274
274
|
|
|
275
275
|
|
|
276
|
-
# Domain Exception pattern for testing (Best Practice example)
|
|
277
|
-
class DomainException(Exception):
|
|
278
|
-
"""Base exception implementing ErrorDTO protocol."""
|
|
279
|
-
status_code: int
|
|
280
|
-
message: str
|
|
281
|
-
|
|
282
|
-
def to_example(self) -> Dict[str, Any]:
|
|
283
|
-
"""Generate example for OpenAPI."""
|
|
284
|
-
return {
|
|
285
|
-
self.message: {
|
|
286
|
-
"value": {"detail": self.message},
|
|
287
|
-
},
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
@classmethod
|
|
291
|
-
def for_openapi(cls):
|
|
292
|
-
"""Returns instance for OpenAPI documentation."""
|
|
293
|
-
return cls()
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
class TestItemNotFoundError(DomainException):
|
|
297
|
-
"""Test item not found error."""
|
|
298
|
-
status_code = status.HTTP_404_NOT_FOUND
|
|
299
|
-
message = "Test item not found"
|
|
300
|
-
|
|
301
|
-
def __init__(self, item_id: str = ""):
|
|
302
|
-
self.item_id = item_id
|
|
303
|
-
super().__init__(self.message)
|
|
304
|
-
|
|
305
|
-
@classmethod
|
|
306
|
-
def for_openapi(cls):
|
|
307
|
-
"""Returns instance for OpenAPI documentation."""
|
|
308
|
-
return cls(item_id="test_id")
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
class TestItemAccessDeniedError(DomainException):
|
|
312
|
-
"""Test item access denied error."""
|
|
313
|
-
status_code = status.HTTP_403_FORBIDDEN
|
|
314
|
-
message = "Test item access denied"
|
|
315
|
-
|
|
316
|
-
def __init__(self, item_id: str = "", user_id: str = ""):
|
|
317
|
-
self.item_id = item_id
|
|
318
|
-
self.user_id = user_id
|
|
319
|
-
super().__init__(self.message)
|
|
320
|
-
|
|
321
|
-
@classmethod
|
|
322
|
-
def for_openapi(cls):
|
|
323
|
-
"""Returns instance for OpenAPI documentation."""
|
|
324
|
-
return cls(item_id="test_id", user_id="test_user")
|
|
325
|
-
|
|
326
|
-
|
|
327
276
|
# Example 11: Domain Exception as ErrorDTO (Best Practice pattern)
|
|
328
277
|
@router.delete(
|
|
329
278
|
"/domain-exception/{item_id}",
|
|
330
279
|
responses=Errors(
|
|
331
|
-
|
|
332
|
-
|
|
280
|
+
MockItemNotFoundError.for_openapi(), # Using for_openapi() pattern
|
|
281
|
+
MockItemAccessDeniedError.for_openapi(), # Using for_openapi() pattern
|
|
333
282
|
unauthorized_401=True, # Standard flag
|
|
334
283
|
),
|
|
335
284
|
)
|
|
@@ -6,6 +6,7 @@ from fastapi import status
|
|
|
6
6
|
from fastapi_errors_plus import BaseErrorDTO, Errors, StandardErrorDTO
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
@pytest.mark.unit
|
|
9
10
|
class TestBaseErrorDTO:
|
|
10
11
|
"""Tests for BaseErrorDTO class."""
|
|
11
12
|
|
|
@@ -69,6 +70,7 @@ class TestBaseErrorDTO:
|
|
|
69
70
|
assert examples["Item not found"]["value"] == {"detail": "Item not found"}
|
|
70
71
|
|
|
71
72
|
|
|
73
|
+
@pytest.mark.unit
|
|
72
74
|
class TestStandardErrorDTO:
|
|
73
75
|
"""Tests for StandardErrorDTO class."""
|
|
74
76
|
|
|
@@ -197,6 +199,7 @@ class TestStandardErrorDTO:
|
|
|
197
199
|
assert "InvalidToken" in examples # From StandardErrorDTO
|
|
198
200
|
|
|
199
201
|
|
|
202
|
+
@pytest.mark.unit
|
|
200
203
|
class TestBaseErrorDTOInheritance:
|
|
201
204
|
"""Tests for inheritance from BaseErrorDTO."""
|
|
202
205
|
|
|
@@ -254,6 +257,7 @@ class TestBaseErrorDTOInheritance:
|
|
|
254
257
|
assert "Custom" in examples
|
|
255
258
|
|
|
256
259
|
|
|
260
|
+
@pytest.mark.unit
|
|
257
261
|
class TestBaseErrorDTOCompatibility:
|
|
258
262
|
"""Tests for compatibility with structural typing (Protocol)."""
|
|
259
263
|
|
|
@@ -304,3 +308,4 @@ class TestBaseErrorDTOCompatibility:
|
|
|
304
308
|
|
|
305
309
|
|
|
306
310
|
|
|
311
|
+
|
|
@@ -9,6 +9,7 @@ from fastapi_errors_plus import Errors
|
|
|
9
9
|
from tests.conftest import SimpleErrorDTO
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
@pytest.mark.unit
|
|
12
13
|
class TestErrorsStandardFlags:
|
|
13
14
|
"""Tests for standard HTTP status flags."""
|
|
14
15
|
|
|
@@ -36,8 +37,8 @@ class TestErrorsStandardFlags:
|
|
|
36
37
|
errors = Errors(validation_error=True)
|
|
37
38
|
responses = errors
|
|
38
39
|
|
|
39
|
-
assert status.
|
|
40
|
-
assert responses[status.
|
|
40
|
+
assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
|
|
41
|
+
assert responses[status.HTTP_422_UNPROCESSABLE_CONTENT]["description"] == "Validation Error"
|
|
41
42
|
|
|
42
43
|
def test_internal_server_error_flag(self):
|
|
43
44
|
"""Test generation of 500 Internal Server Error from flag."""
|
|
@@ -68,8 +69,8 @@ class TestErrorsStandardFlags:
|
|
|
68
69
|
errors = Errors(validation_error_422=True)
|
|
69
70
|
responses = errors
|
|
70
71
|
|
|
71
|
-
assert status.
|
|
72
|
-
assert responses[status.
|
|
72
|
+
assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
|
|
73
|
+
assert responses[status.HTTP_422_UNPROCESSABLE_CONTENT]["description"] == "Validation Error"
|
|
73
74
|
|
|
74
75
|
def test_internal_server_error_500_flag(self):
|
|
75
76
|
"""Test generation of 500 Internal Server Error from explicit flag."""
|
|
@@ -108,7 +109,7 @@ class TestErrorsStandardFlags:
|
|
|
108
109
|
|
|
109
110
|
assert status.HTTP_401_UNAUTHORIZED in responses
|
|
110
111
|
assert status.HTTP_403_FORBIDDEN in responses
|
|
111
|
-
assert status.
|
|
112
|
+
assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
|
|
112
113
|
assert status.HTTP_500_INTERNAL_SERVER_ERROR in responses
|
|
113
114
|
assert len(responses) == 4
|
|
114
115
|
|
|
@@ -133,11 +134,12 @@ class TestErrorsStandardFlags:
|
|
|
133
134
|
|
|
134
135
|
assert status.HTTP_401_UNAUTHORIZED in responses
|
|
135
136
|
assert status.HTTP_403_FORBIDDEN in responses
|
|
136
|
-
assert status.
|
|
137
|
+
assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
|
|
137
138
|
assert status.HTTP_500_INTERNAL_SERVER_ERROR in responses
|
|
138
139
|
assert len(responses) == 4
|
|
139
140
|
|
|
140
141
|
|
|
142
|
+
@pytest.mark.unit
|
|
141
143
|
class TestErrorsDict:
|
|
142
144
|
"""Tests for dict-based errors."""
|
|
143
145
|
|
|
@@ -214,6 +216,7 @@ class TestErrorsDict:
|
|
|
214
216
|
assert "SessionNotFound" in examples
|
|
215
217
|
|
|
216
218
|
|
|
219
|
+
@pytest.mark.unit
|
|
217
220
|
class TestErrorsErrorDTO:
|
|
218
221
|
"""Tests for ErrorDTO-based errors."""
|
|
219
222
|
|
|
@@ -240,6 +243,7 @@ class TestErrorsErrorDTO:
|
|
|
240
243
|
assert len(responses) == 2
|
|
241
244
|
|
|
242
245
|
|
|
246
|
+
@pytest.mark.unit
|
|
243
247
|
class TestErrorsMergeExamples:
|
|
244
248
|
"""Tests for merging examples for the same status code."""
|
|
245
249
|
|
|
@@ -296,6 +300,7 @@ class TestErrorsMergeExamples:
|
|
|
296
300
|
assert "examples" in content or "example" in content
|
|
297
301
|
|
|
298
302
|
|
|
303
|
+
@pytest.mark.unit
|
|
299
304
|
class TestErrorsMixed:
|
|
300
305
|
"""Tests for mixed usage (flags + dict + ErrorDTO)."""
|
|
301
306
|
|
|
@@ -327,6 +332,7 @@ class TestErrorsMixed:
|
|
|
327
332
|
assert len(responses) == 4
|
|
328
333
|
|
|
329
334
|
|
|
335
|
+
@pytest.mark.unit
|
|
330
336
|
class TestErrorsEdgeCases:
|
|
331
337
|
"""Tests for edge cases."""
|
|
332
338
|
|
|
@@ -337,7 +343,7 @@ class TestErrorsEdgeCases:
|
|
|
337
343
|
|
|
338
344
|
assert isinstance(responses, Mapping)
|
|
339
345
|
# validation_error=True by default, so 422 should be present
|
|
340
|
-
assert status.
|
|
346
|
+
assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
|
|
341
347
|
assert len(responses) == 1
|
|
342
348
|
|
|
343
349
|
def test_validation_error_default_true(self):
|
|
@@ -345,15 +351,15 @@ class TestErrorsEdgeCases:
|
|
|
345
351
|
errors = Errors()
|
|
346
352
|
responses = errors
|
|
347
353
|
|
|
348
|
-
assert status.
|
|
349
|
-
assert responses[status.
|
|
354
|
+
assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
|
|
355
|
+
assert responses[status.HTTP_422_UNPROCESSABLE_CONTENT]["description"] == "Validation Error"
|
|
350
356
|
|
|
351
357
|
def test_validation_error_can_be_disabled(self):
|
|
352
358
|
"""Test that validation_error can be explicitly set to False."""
|
|
353
359
|
errors = Errors(validation_error=False)
|
|
354
360
|
responses = errors
|
|
355
361
|
|
|
356
|
-
assert status.
|
|
362
|
+
assert status.HTTP_422_UNPROCESSABLE_CONTENT not in responses
|
|
357
363
|
assert len(responses) == 0
|
|
358
364
|
|
|
359
365
|
def test_validation_error_422_default_true(self):
|
|
@@ -361,28 +367,28 @@ class TestErrorsEdgeCases:
|
|
|
361
367
|
errors = Errors()
|
|
362
368
|
responses = errors
|
|
363
369
|
|
|
364
|
-
assert status.
|
|
370
|
+
assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
|
|
365
371
|
|
|
366
372
|
def test_validation_error_422_can_be_disabled(self):
|
|
367
373
|
"""Test that validation_error_422 can be explicitly set to False."""
|
|
368
374
|
errors = Errors(validation_error_422=False)
|
|
369
375
|
responses = errors
|
|
370
376
|
|
|
371
|
-
assert status.
|
|
377
|
+
assert status.HTTP_422_UNPROCESSABLE_CONTENT not in responses
|
|
372
378
|
|
|
373
379
|
def test_validation_error_explicit_true_still_works(self):
|
|
374
380
|
"""Test that explicit validation_error=True still works (backward compatibility)."""
|
|
375
381
|
errors = Errors(validation_error=True)
|
|
376
382
|
responses = errors
|
|
377
383
|
|
|
378
|
-
assert status.
|
|
384
|
+
assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
|
|
379
385
|
|
|
380
386
|
def test_validation_error_old_and_new_flags(self):
|
|
381
387
|
"""Test that both old and new validation_error flags work together."""
|
|
382
388
|
errors = Errors(validation_error=False, validation_error_422=False)
|
|
383
389
|
responses = errors
|
|
384
390
|
|
|
385
|
-
assert status.
|
|
391
|
+
assert status.HTTP_422_UNPROCESSABLE_CONTENT not in responses
|
|
386
392
|
|
|
387
393
|
def test_validation_error_with_other_errors(self):
|
|
388
394
|
"""Test validation_error=True by default with other errors."""
|
|
@@ -391,7 +397,7 @@ class TestErrorsEdgeCases:
|
|
|
391
397
|
|
|
392
398
|
assert status.HTTP_401_UNAUTHORIZED in responses
|
|
393
399
|
assert status.HTTP_403_FORBIDDEN in responses
|
|
394
|
-
assert status.
|
|
400
|
+
assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses # Default True
|
|
395
401
|
assert len(responses) == 3
|
|
396
402
|
|
|
397
403
|
def test_no_flags_no_errors(self):
|
|
@@ -446,6 +452,7 @@ class TestErrorsEdgeCases:
|
|
|
446
452
|
assert len(errors) == 1
|
|
447
453
|
|
|
448
454
|
|
|
455
|
+
@pytest.mark.unit
|
|
449
456
|
class TestUniqueKeys:
|
|
450
457
|
"""Tests for unique key generation to prevent collisions."""
|
|
451
458
|
|
|
@@ -522,6 +529,7 @@ class TestUniqueKeys:
|
|
|
522
529
|
assert unique_key == "CustomExample_4"
|
|
523
530
|
|
|
524
531
|
|
|
532
|
+
@pytest.mark.unit
|
|
525
533
|
class TestErrorsValidation:
|
|
526
534
|
"""Tests for ErrorDTO validation."""
|
|
527
535
|
|
|
@@ -608,6 +616,7 @@ class TestErrorsValidation:
|
|
|
608
616
|
assert "BadObject" in error_msg
|
|
609
617
|
|
|
610
618
|
|
|
619
|
+
@pytest.mark.unit
|
|
611
620
|
class TestDTODescriptionPriority:
|
|
612
621
|
"""Tests for DTO description priority over standard flags."""
|
|
613
622
|
|
|
@@ -654,3 +663,93 @@ class TestDTODescriptionPriority:
|
|
|
654
663
|
# Dict description should be preserved (dict > DTO)
|
|
655
664
|
assert errors[401]["description"] == "My Custom Description"
|
|
656
665
|
|
|
666
|
+
|
|
667
|
+
@pytest.mark.unit
|
|
668
|
+
class TestPydanticIntegration:
|
|
669
|
+
"""Tests for Pydantic model integration with ErrorDTO Protocol.
|
|
670
|
+
|
|
671
|
+
These tests are optional - they will be skipped if Pydantic is not installed.
|
|
672
|
+
This demonstrates that the library works with Pydantic models through structural typing,
|
|
673
|
+
but Pydantic is not a required dependency.
|
|
674
|
+
"""
|
|
675
|
+
|
|
676
|
+
def test_pydantic_model_as_error_dto(self):
|
|
677
|
+
"""Test that Pydantic models work with ErrorDTO Protocol (with proper skip)."""
|
|
678
|
+
try:
|
|
679
|
+
from pydantic import BaseModel, Field
|
|
680
|
+
from typing import Dict, Any
|
|
681
|
+
except ImportError:
|
|
682
|
+
pytest.skip("Pydantic is not installed - this is expected and OK")
|
|
683
|
+
|
|
684
|
+
class PydanticErrorDTO(BaseModel):
|
|
685
|
+
"""Pydantic model implementing ErrorDTO Protocol."""
|
|
686
|
+
status_code: int = Field(..., ge=400, le=599)
|
|
687
|
+
message: str = Field(..., min_length=1)
|
|
688
|
+
|
|
689
|
+
def to_example(self) -> Dict[str, Any]:
|
|
690
|
+
"""Generate example for OpenAPI."""
|
|
691
|
+
return {
|
|
692
|
+
self.message: {
|
|
693
|
+
"value": {"detail": self.message},
|
|
694
|
+
},
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
# Create instance
|
|
698
|
+
error = PydanticErrorDTO(
|
|
699
|
+
status_code=404,
|
|
700
|
+
message="Not found",
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
# Use with Errors class
|
|
704
|
+
errors = Errors(error, validation_error=False)
|
|
705
|
+
responses = errors
|
|
706
|
+
|
|
707
|
+
assert status.HTTP_404_NOT_FOUND in responses
|
|
708
|
+
assert responses[status.HTTP_404_NOT_FOUND]["description"] == "Not found"
|
|
709
|
+
|
|
710
|
+
def test_pydantic_model_with_additional_fields(self):
|
|
711
|
+
"""Test Pydantic model with additional fields."""
|
|
712
|
+
try:
|
|
713
|
+
from pydantic import BaseModel, Field
|
|
714
|
+
from typing import Dict, Any, Optional
|
|
715
|
+
except ImportError:
|
|
716
|
+
pytest.skip("Pydantic is not installed - this is expected and OK")
|
|
717
|
+
|
|
718
|
+
class DetailedErrorDTO(BaseModel):
|
|
719
|
+
"""Pydantic model for errors with additional fields."""
|
|
720
|
+
status_code: int = Field(..., ge=400, le=599)
|
|
721
|
+
message: str = Field(..., min_length=1)
|
|
722
|
+
error_code: Optional[str] = Field(None, description="Internal error code")
|
|
723
|
+
|
|
724
|
+
def to_example(self) -> Dict[str, Any]:
|
|
725
|
+
"""Generate example for OpenAPI."""
|
|
726
|
+
example = {"detail": self.message}
|
|
727
|
+
if self.error_code:
|
|
728
|
+
example["error_code"] = self.error_code
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
self.message: {
|
|
732
|
+
"value": example,
|
|
733
|
+
},
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
# Create instance
|
|
737
|
+
error = DetailedErrorDTO(
|
|
738
|
+
status_code=422,
|
|
739
|
+
message="Validation failed",
|
|
740
|
+
error_code="VALIDATION_ERROR",
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
# Use with Errors class
|
|
744
|
+
errors = Errors(error, validation_error=False)
|
|
745
|
+
responses = errors
|
|
746
|
+
|
|
747
|
+
assert status.HTTP_422_UNPROCESSABLE_CONTENT in responses
|
|
748
|
+
assert responses[status.HTTP_422_UNPROCESSABLE_CONTENT]["description"] == "Validation failed"
|
|
749
|
+
|
|
750
|
+
# Check that error_code is in the example
|
|
751
|
+
examples = responses[status.HTTP_422_UNPROCESSABLE_CONTENT]["content"]["application/json"]["examples"]
|
|
752
|
+
example_value = examples["Validation failed"]["value"]
|
|
753
|
+
assert example_value["detail"] == "Validation failed"
|
|
754
|
+
assert example_value["error_code"] == "VALIDATION_ERROR"
|
|
755
|
+
|
|
@@ -4,8 +4,10 @@ from fastapi import status
|
|
|
4
4
|
from fastapi.testclient import TestClient
|
|
5
5
|
|
|
6
6
|
from tests.test_app import app
|
|
7
|
+
import pytest
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
@pytest.mark.integration
|
|
9
11
|
class TestOpenAPIGeneration:
|
|
10
12
|
"""Tests for OpenAPI specification generation."""
|
|
11
13
|
|
|
@@ -39,6 +41,7 @@ class TestOpenAPIGeneration:
|
|
|
39
41
|
assert "/api/v1/mixed-base-dto/{item_id}" in paths
|
|
40
42
|
|
|
41
43
|
|
|
44
|
+
@pytest.mark.integration
|
|
42
45
|
class TestStandardFlagsEndpoint:
|
|
43
46
|
"""Tests for standard flags endpoint."""
|
|
44
47
|
|
|
@@ -71,6 +74,7 @@ class TestStandardFlagsEndpoint:
|
|
|
71
74
|
assert "example" in error_401["content"]["application/json"]
|
|
72
75
|
|
|
73
76
|
|
|
77
|
+
@pytest.mark.integration
|
|
74
78
|
class TestDictErrorsEndpoint:
|
|
75
79
|
"""Tests for dict errors endpoint."""
|
|
76
80
|
|
|
@@ -100,6 +104,7 @@ class TestDictErrorsEndpoint:
|
|
|
100
104
|
assert "application/json" in error_404["content"]
|
|
101
105
|
|
|
102
106
|
|
|
107
|
+
@pytest.mark.integration
|
|
103
108
|
class TestErrorDTOEndpoint:
|
|
104
109
|
"""Tests for ErrorDTO endpoint."""
|
|
105
110
|
|
|
@@ -129,6 +134,7 @@ class TestErrorDTOEndpoint:
|
|
|
129
134
|
assert "examples" in error_404["content"]["application/json"]
|
|
130
135
|
|
|
131
136
|
|
|
137
|
+
@pytest.mark.integration
|
|
132
138
|
class TestMixedEndpoint:
|
|
133
139
|
"""Tests for mixed endpoint (flags + dict + ErrorDTO)."""
|
|
134
140
|
|
|
@@ -166,6 +172,7 @@ class TestMixedEndpoint:
|
|
|
166
172
|
assert responses["409"]["description"] == "Conflict"
|
|
167
173
|
|
|
168
174
|
|
|
175
|
+
@pytest.mark.integration
|
|
169
176
|
class TestMergeExamplesEndpoint:
|
|
170
177
|
"""Tests for merge examples endpoint."""
|
|
171
178
|
|
|
@@ -187,6 +194,7 @@ class TestMergeExamplesEndpoint:
|
|
|
187
194
|
assert "Error 2" in examples
|
|
188
195
|
|
|
189
196
|
|
|
197
|
+
@pytest.mark.integration
|
|
190
198
|
class TestEmptyErrorsEndpoint:
|
|
191
199
|
"""Tests for empty errors endpoint."""
|
|
192
200
|
|
|
@@ -206,6 +214,7 @@ class TestEmptyErrorsEndpoint:
|
|
|
206
214
|
assert len(error_codes) == 0
|
|
207
215
|
|
|
208
216
|
|
|
217
|
+
@pytest.mark.integration
|
|
209
218
|
class TestMergeFlagDictEndpoint:
|
|
210
219
|
"""Tests for merge flag and dict endpoint."""
|
|
211
220
|
|
|
@@ -226,6 +235,7 @@ class TestMergeFlagDictEndpoint:
|
|
|
226
235
|
assert "SessionNotFound" in examples
|
|
227
236
|
|
|
228
237
|
|
|
238
|
+
@pytest.mark.integration
|
|
229
239
|
class TestBaseErrorDTOEndpoint:
|
|
230
240
|
"""Tests for BaseErrorDTO endpoint."""
|
|
231
241
|
|
|
@@ -257,6 +267,7 @@ class TestBaseErrorDTOEndpoint:
|
|
|
257
267
|
assert "Item not found" in examples
|
|
258
268
|
|
|
259
269
|
|
|
270
|
+
@pytest.mark.integration
|
|
260
271
|
class TestStandardErrorDTOEndpoint:
|
|
261
272
|
"""Tests for StandardErrorDTO endpoint."""
|
|
262
273
|
|
|
@@ -301,6 +312,7 @@ class TestStandardErrorDTOEndpoint:
|
|
|
301
312
|
assert "RoleHasNoAccess" in examples
|
|
302
313
|
|
|
303
314
|
|
|
315
|
+
@pytest.mark.integration
|
|
304
316
|
class TestMixedBaseDTOEndpoint:
|
|
305
317
|
"""Tests for mixed BaseErrorDTO + StandardErrorDTO + flags endpoint."""
|
|
306
318
|
|
|
@@ -319,6 +331,7 @@ class TestMixedBaseDTOEndpoint:
|
|
|
319
331
|
assert "500" in responses # From flag
|
|
320
332
|
|
|
321
333
|
|
|
334
|
+
@pytest.mark.integration
|
|
322
335
|
class TestDomainExceptionEndpoint:
|
|
323
336
|
"""Tests for Domain Exception as ErrorDTO endpoint (Best Practice pattern)."""
|
|
324
337
|
|
|
@@ -332,8 +345,8 @@ class TestDomainExceptionEndpoint:
|
|
|
332
345
|
responses = endpoint["responses"]
|
|
333
346
|
|
|
334
347
|
assert "401" in responses # From flag
|
|
335
|
-
assert "403" in responses # From
|
|
336
|
-
assert "404" in responses # From
|
|
348
|
+
assert "403" in responses # From MockItemAccessDeniedError.for_openapi()
|
|
349
|
+
assert "404" in responses # From MockItemNotFoundError.for_openapi()
|
|
337
350
|
|
|
338
351
|
def test_404_response_structure_from_domain_exception(self):
|
|
339
352
|
"""Test 404 response structure from Domain Exception."""
|
|
@@ -369,9 +382,9 @@ class TestDomainExceptionEndpoint:
|
|
|
369
382
|
|
|
370
383
|
def test_for_openapi_method_returns_error_dto(self):
|
|
371
384
|
"""Test that for_openapi() method returns instance implementing ErrorDTO."""
|
|
372
|
-
from tests.test_app import
|
|
385
|
+
from tests.test_app import MockItemNotFoundError
|
|
373
386
|
|
|
374
|
-
error_instance =
|
|
387
|
+
error_instance = MockItemNotFoundError.for_openapi()
|
|
375
388
|
|
|
376
389
|
# Check that it implements ErrorDTO protocol
|
|
377
390
|
assert hasattr(error_instance, "status_code")
|
|
@@ -392,11 +405,11 @@ class TestDomainExceptionEndpoint:
|
|
|
392
405
|
def test_domain_exception_works_with_errors_class(self):
|
|
393
406
|
"""Test that Domain Exception instances work with Errors class."""
|
|
394
407
|
from fastapi_errors_plus import Errors
|
|
395
|
-
from tests.test_app import
|
|
408
|
+
from tests.test_app import MockItemNotFoundError, MockItemAccessDeniedError
|
|
396
409
|
|
|
397
410
|
errors = Errors(
|
|
398
|
-
|
|
399
|
-
|
|
411
|
+
MockItemNotFoundError.for_openapi(),
|
|
412
|
+
MockItemAccessDeniedError.for_openapi(),
|
|
400
413
|
)
|
|
401
414
|
responses = errors
|
|
402
415
|
|
|
@@ -406,6 +419,7 @@ class TestDomainExceptionEndpoint:
|
|
|
406
419
|
assert responses[403]["description"] == "Test item access denied"
|
|
407
420
|
|
|
408
421
|
|
|
422
|
+
@pytest.mark.integration
|
|
409
423
|
class TestRealEndpoints:
|
|
410
424
|
"""Tests for actual endpoint responses."""
|
|
411
425
|
|
|
File without changes
|
{fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus.egg-info/requires.txt
RENAMED
|
File without changes
|
{fastapi_errors_plus-0.6.1 → fastapi_errors_plus-0.6.2}/fastapi_errors_plus.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|