django-bolt 0.1.0__cp310-abi3-win_amd64.whl → 0.1.1__cp310-abi3-win_amd64.whl
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.
Potentially problematic release.
This version of django-bolt might be problematic. Click here for more details.
- django_bolt/__init__.py +2 -2
- django_bolt/_core.pyd +0 -0
- django_bolt/_json.py +169 -0
- django_bolt/admin/static_routes.py +15 -21
- django_bolt/api.py +181 -61
- django_bolt/auth/__init__.py +2 -2
- django_bolt/decorators.py +15 -3
- django_bolt/dependencies.py +30 -24
- django_bolt/error_handlers.py +2 -1
- django_bolt/openapi/plugins.py +3 -2
- django_bolt/openapi/schema_generator.py +65 -20
- django_bolt/pagination.py +2 -1
- django_bolt/responses.py +3 -2
- django_bolt/serialization.py +5 -4
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.1.dist-info}/METADATA +179 -197
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.1.dist-info}/RECORD +18 -55
- django_bolt/auth/README.md +0 -464
- django_bolt/auth/REVOCATION_EXAMPLE.md +0 -391
- django_bolt/tests/__init__.py +0 -0
- django_bolt/tests/admin_tests/__init__.py +0 -1
- django_bolt/tests/admin_tests/conftest.py +0 -6
- django_bolt/tests/admin_tests/test_admin_with_django.py +0 -278
- django_bolt/tests/admin_tests/urls.py +0 -9
- django_bolt/tests/cbv/__init__.py +0 -0
- django_bolt/tests/cbv/test_class_views.py +0 -570
- django_bolt/tests/cbv/test_class_views_django_orm.py +0 -703
- django_bolt/tests/cbv/test_class_views_features.py +0 -1173
- django_bolt/tests/cbv/test_class_views_with_client.py +0 -622
- django_bolt/tests/conftest.py +0 -165
- django_bolt/tests/test_action_decorator.py +0 -399
- django_bolt/tests/test_auth_secret_key.py +0 -83
- django_bolt/tests/test_decorator_syntax.py +0 -159
- django_bolt/tests/test_error_handling.py +0 -481
- django_bolt/tests/test_file_response.py +0 -192
- django_bolt/tests/test_global_cors.py +0 -172
- django_bolt/tests/test_guards_auth.py +0 -441
- django_bolt/tests/test_guards_integration.py +0 -303
- django_bolt/tests/test_health.py +0 -283
- django_bolt/tests/test_integration_validation.py +0 -400
- django_bolt/tests/test_json_validation.py +0 -536
- django_bolt/tests/test_jwt_auth.py +0 -327
- django_bolt/tests/test_jwt_token.py +0 -458
- django_bolt/tests/test_logging.py +0 -837
- django_bolt/tests/test_logging_merge.py +0 -419
- django_bolt/tests/test_middleware.py +0 -492
- django_bolt/tests/test_middleware_server.py +0 -230
- django_bolt/tests/test_model_viewset.py +0 -323
- django_bolt/tests/test_models.py +0 -24
- django_bolt/tests/test_pagination.py +0 -1258
- django_bolt/tests/test_parameter_validation.py +0 -178
- django_bolt/tests/test_syntax.py +0 -626
- django_bolt/tests/test_testing_utilities.py +0 -163
- django_bolt/tests/test_testing_utilities_simple.py +0 -123
- django_bolt/tests/test_viewset_unified.py +0 -346
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.1.dist-info}/WHEEL +0 -0
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tests for decorator syntax (@api.view and @api.viewset).
|
|
3
|
-
|
|
4
|
-
This test suite verifies that the decorator pattern works correctly.
|
|
5
|
-
"""
|
|
6
|
-
import pytest
|
|
7
|
-
import msgspec
|
|
8
|
-
from django_bolt import BoltAPI, ViewSet, action
|
|
9
|
-
from django_bolt.views import APIView
|
|
10
|
-
from django_bolt.testing import TestClient
|
|
11
|
-
from .test_models import Article
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# --- Fixtures ---
|
|
15
|
-
|
|
16
|
-
@pytest.fixture
|
|
17
|
-
def api():
|
|
18
|
-
"""Create a fresh BoltAPI instance for each test."""
|
|
19
|
-
return BoltAPI()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# --- Tests ---
|
|
23
|
-
|
|
24
|
-
def test_view_decorator_syntax(api):
|
|
25
|
-
"""Test @api.view() decorator syntax."""
|
|
26
|
-
|
|
27
|
-
@api.view("/health")
|
|
28
|
-
class HealthView(APIView):
|
|
29
|
-
async def get(self, request):
|
|
30
|
-
return {"status": "healthy"}
|
|
31
|
-
|
|
32
|
-
client = TestClient(api)
|
|
33
|
-
response = client.get("/health")
|
|
34
|
-
assert response.status_code == 200
|
|
35
|
-
assert response.json()["status"] == "healthy"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_view_decorator_with_multiple_methods(api):
|
|
39
|
-
"""Test @api.view() with multiple HTTP methods."""
|
|
40
|
-
|
|
41
|
-
class ItemData(msgspec.Struct):
|
|
42
|
-
name: str
|
|
43
|
-
|
|
44
|
-
@api.view("/items")
|
|
45
|
-
class ItemView(APIView):
|
|
46
|
-
async def get(self, request):
|
|
47
|
-
return {"items": ["item1", "item2"]}
|
|
48
|
-
|
|
49
|
-
async def post(self, request, data: ItemData):
|
|
50
|
-
return {"created": True, "item": data.name}
|
|
51
|
-
|
|
52
|
-
client = TestClient(api)
|
|
53
|
-
|
|
54
|
-
response = client.get("/items")
|
|
55
|
-
assert response.status_code == 200
|
|
56
|
-
assert "items" in response.json()
|
|
57
|
-
|
|
58
|
-
response = client.post("/items", json={"name": "test"})
|
|
59
|
-
assert response.status_code == 200
|
|
60
|
-
assert response.json()["created"] is True
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@pytest.mark.django_db(transaction=True)
|
|
64
|
-
def test_viewset_decorator_syntax(api):
|
|
65
|
-
"""Test @api.viewset() decorator syntax."""
|
|
66
|
-
|
|
67
|
-
class ArticleSchema(msgspec.Struct):
|
|
68
|
-
id: int
|
|
69
|
-
title: str
|
|
70
|
-
|
|
71
|
-
@api.viewset("/articles")
|
|
72
|
-
class ArticleViewSet(ViewSet):
|
|
73
|
-
queryset = Article.objects.all()
|
|
74
|
-
serializer_class = ArticleSchema
|
|
75
|
-
|
|
76
|
-
async def list(self, request):
|
|
77
|
-
return []
|
|
78
|
-
|
|
79
|
-
async def retrieve(self, request, pk: int):
|
|
80
|
-
article = await self.get_object(pk)
|
|
81
|
-
return ArticleSchema(id=article.id, title=article.title)
|
|
82
|
-
|
|
83
|
-
# Create test article
|
|
84
|
-
article = Article.objects.create(
|
|
85
|
-
title="Test Article",
|
|
86
|
-
content="Test content",
|
|
87
|
-
author="Test Author"
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
client = TestClient(api)
|
|
91
|
-
|
|
92
|
-
# Test list
|
|
93
|
-
response = client.get("/articles")
|
|
94
|
-
assert response.status_code == 200
|
|
95
|
-
|
|
96
|
-
# Test retrieve
|
|
97
|
-
response = client.get(f"/articles/{article.id}")
|
|
98
|
-
assert response.status_code == 200
|
|
99
|
-
data = response.json()
|
|
100
|
-
assert data["title"] == "Test Article"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
@pytest.mark.django_db(transaction=True)
|
|
104
|
-
def test_viewset_decorator_with_custom_actions(api):
|
|
105
|
-
"""Test @api.viewset() decorator with @action decorator."""
|
|
106
|
-
|
|
107
|
-
class ArticleSchema(msgspec.Struct):
|
|
108
|
-
id: int
|
|
109
|
-
title: str
|
|
110
|
-
|
|
111
|
-
@api.viewset("/articles")
|
|
112
|
-
class ArticleViewSet(ViewSet):
|
|
113
|
-
queryset = Article.objects.all()
|
|
114
|
-
|
|
115
|
-
async def list(self, request):
|
|
116
|
-
return []
|
|
117
|
-
|
|
118
|
-
@action(methods=["POST"], detail=True)
|
|
119
|
-
async def publish(self, request, pk: int):
|
|
120
|
-
article = await self.get_object(pk)
|
|
121
|
-
article.is_published = True
|
|
122
|
-
await article.asave()
|
|
123
|
-
return {"published": True, "article_id": pk}
|
|
124
|
-
|
|
125
|
-
@action(methods=["GET"], detail=False)
|
|
126
|
-
async def published(self, request):
|
|
127
|
-
articles = []
|
|
128
|
-
async for article in Article.objects.filter(is_published=True):
|
|
129
|
-
articles.append(ArticleSchema(id=article.id, title=article.title))
|
|
130
|
-
return articles
|
|
131
|
-
|
|
132
|
-
# Create test articles
|
|
133
|
-
article1 = Article.objects.create(
|
|
134
|
-
title="Published",
|
|
135
|
-
content="Content",
|
|
136
|
-
author="Author",
|
|
137
|
-
is_published=True
|
|
138
|
-
)
|
|
139
|
-
article2 = Article.objects.create(
|
|
140
|
-
title="Draft",
|
|
141
|
-
content="Content",
|
|
142
|
-
author="Author",
|
|
143
|
-
is_published=False
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
client = TestClient(api)
|
|
147
|
-
|
|
148
|
-
# Test custom action: publish
|
|
149
|
-
response = client.post(f"/articles/{article2.id}/publish")
|
|
150
|
-
assert response.status_code == 200
|
|
151
|
-
assert response.json()["published"] is True
|
|
152
|
-
|
|
153
|
-
# Test custom action: published
|
|
154
|
-
response = client.get("/articles/published")
|
|
155
|
-
assert response.status_code == 200
|
|
156
|
-
data = response.json()
|
|
157
|
-
assert len(data) == 2 # Both should be published now
|
|
158
|
-
|
|
159
|
-
|
|
@@ -1,481 +0,0 @@
|
|
|
1
|
-
"""Tests for Django-Bolt error handling system."""
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
import msgspec
|
|
5
|
-
from django_bolt.exceptions import (
|
|
6
|
-
HTTPException,
|
|
7
|
-
BadRequest,
|
|
8
|
-
Unauthorized,
|
|
9
|
-
Forbidden,
|
|
10
|
-
NotFound,
|
|
11
|
-
UnprocessableEntity,
|
|
12
|
-
TooManyRequests,
|
|
13
|
-
InternalServerError,
|
|
14
|
-
ServiceUnavailable,
|
|
15
|
-
RequestValidationError,
|
|
16
|
-
ResponseValidationError,
|
|
17
|
-
)
|
|
18
|
-
from django_bolt.error_handlers import (
|
|
19
|
-
format_error_response,
|
|
20
|
-
http_exception_handler,
|
|
21
|
-
request_validation_error_handler,
|
|
22
|
-
response_validation_error_handler,
|
|
23
|
-
msgspec_validation_error_to_dict,
|
|
24
|
-
generic_exception_handler,
|
|
25
|
-
handle_exception,
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class TestExceptions:
|
|
30
|
-
"""Test exception classes."""
|
|
31
|
-
|
|
32
|
-
def test_http_exception_basic(self):
|
|
33
|
-
"""Test basic HTTPException."""
|
|
34
|
-
exc = HTTPException(status_code=400, detail="Bad request")
|
|
35
|
-
assert exc.status_code == 400
|
|
36
|
-
assert exc.detail == "Bad request"
|
|
37
|
-
assert exc.headers == {}
|
|
38
|
-
assert exc.extra is None
|
|
39
|
-
|
|
40
|
-
def test_http_exception_with_headers(self):
|
|
41
|
-
"""Test HTTPException with custom headers."""
|
|
42
|
-
exc = HTTPException(
|
|
43
|
-
status_code=401,
|
|
44
|
-
detail="Unauthorized",
|
|
45
|
-
headers={"WWW-Authenticate": "Bearer"}
|
|
46
|
-
)
|
|
47
|
-
assert exc.status_code == 401
|
|
48
|
-
assert exc.headers == {"WWW-Authenticate": "Bearer"}
|
|
49
|
-
|
|
50
|
-
def test_http_exception_with_extra(self):
|
|
51
|
-
"""Test HTTPException with extra data."""
|
|
52
|
-
exc = HTTPException(
|
|
53
|
-
status_code=422,
|
|
54
|
-
detail="Validation failed",
|
|
55
|
-
extra={"errors": ["field1", "field2"]}
|
|
56
|
-
)
|
|
57
|
-
assert exc.extra == {"errors": ["field1", "field2"]}
|
|
58
|
-
|
|
59
|
-
def test_http_exception_default_message(self):
|
|
60
|
-
"""Test HTTPException uses HTTP status phrase as default."""
|
|
61
|
-
exc = HTTPException(status_code=404)
|
|
62
|
-
assert exc.detail == "Not Found"
|
|
63
|
-
|
|
64
|
-
def test_specialized_exceptions(self):
|
|
65
|
-
"""Test specialized exception classes."""
|
|
66
|
-
assert BadRequest().status_code == 400
|
|
67
|
-
assert Unauthorized().status_code == 401
|
|
68
|
-
assert Forbidden().status_code == 403
|
|
69
|
-
assert NotFound().status_code == 404
|
|
70
|
-
assert UnprocessableEntity().status_code == 422
|
|
71
|
-
assert TooManyRequests().status_code == 429
|
|
72
|
-
assert InternalServerError().status_code == 500
|
|
73
|
-
assert ServiceUnavailable().status_code == 503
|
|
74
|
-
|
|
75
|
-
def test_exception_repr(self):
|
|
76
|
-
"""Test exception __repr__."""
|
|
77
|
-
exc = NotFound(detail="User not found")
|
|
78
|
-
assert "404" in repr(exc)
|
|
79
|
-
assert "NotFound" in repr(exc)
|
|
80
|
-
assert "User not found" in repr(exc)
|
|
81
|
-
|
|
82
|
-
def test_validation_exception(self):
|
|
83
|
-
"""Test ValidationException."""
|
|
84
|
-
errors = [
|
|
85
|
-
{"loc": ["body", "name"], "msg": "Field required", "type": "missing"}
|
|
86
|
-
]
|
|
87
|
-
exc = RequestValidationError(errors)
|
|
88
|
-
assert exc.errors() == errors
|
|
89
|
-
|
|
90
|
-
def test_request_validation_error_with_body(self):
|
|
91
|
-
"""Test RequestValidationError stores body."""
|
|
92
|
-
errors = [{"loc": ["body"], "msg": "Invalid", "type": "value_error"}]
|
|
93
|
-
body = {"test": "data"}
|
|
94
|
-
exc = RequestValidationError(errors, body=body)
|
|
95
|
-
assert exc.body == body
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
class TestErrorHandlers:
|
|
99
|
-
"""Test error handler functions."""
|
|
100
|
-
|
|
101
|
-
def test_format_error_response_simple(self):
|
|
102
|
-
"""Test simple error response formatting."""
|
|
103
|
-
status, headers, body = format_error_response(
|
|
104
|
-
status_code=404,
|
|
105
|
-
detail="Not found"
|
|
106
|
-
)
|
|
107
|
-
assert status == 404
|
|
108
|
-
assert ("content-type", "application/json") in headers
|
|
109
|
-
|
|
110
|
-
# Decode and check JSON
|
|
111
|
-
import json
|
|
112
|
-
data = json.loads(body)
|
|
113
|
-
assert data["detail"] == "Not found"
|
|
114
|
-
|
|
115
|
-
def test_format_error_response_with_extra(self):
|
|
116
|
-
"""Test error response with extra data."""
|
|
117
|
-
status, headers, body = format_error_response(
|
|
118
|
-
status_code=422,
|
|
119
|
-
detail="Validation failed",
|
|
120
|
-
extra={"errors": ["field1"]}
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
import json
|
|
124
|
-
data = json.loads(body)
|
|
125
|
-
assert data["detail"] == "Validation failed"
|
|
126
|
-
assert data["extra"] == {"errors": ["field1"]}
|
|
127
|
-
|
|
128
|
-
def test_http_exception_handler(self):
|
|
129
|
-
"""Test HTTPException handler."""
|
|
130
|
-
exc = NotFound(detail="User not found")
|
|
131
|
-
status, headers, body = http_exception_handler(exc)
|
|
132
|
-
|
|
133
|
-
assert status == 404
|
|
134
|
-
import json
|
|
135
|
-
data = json.loads(body)
|
|
136
|
-
assert data["detail"] == "User not found"
|
|
137
|
-
|
|
138
|
-
def test_http_exception_handler_with_headers(self):
|
|
139
|
-
"""Test HTTPException handler preserves custom headers."""
|
|
140
|
-
exc = Unauthorized(
|
|
141
|
-
detail="Auth required",
|
|
142
|
-
headers={"WWW-Authenticate": "Bearer"}
|
|
143
|
-
)
|
|
144
|
-
status, headers, body = http_exception_handler(exc)
|
|
145
|
-
|
|
146
|
-
assert status == 401
|
|
147
|
-
assert ("WWW-Authenticate", "Bearer") in headers
|
|
148
|
-
|
|
149
|
-
def test_request_validation_error_handler(self):
|
|
150
|
-
"""Test request validation error handler."""
|
|
151
|
-
errors = [
|
|
152
|
-
{
|
|
153
|
-
"loc": ["body", "email"],
|
|
154
|
-
"msg": "Invalid email",
|
|
155
|
-
"type": "value_error"
|
|
156
|
-
}
|
|
157
|
-
]
|
|
158
|
-
exc = RequestValidationError(errors)
|
|
159
|
-
status, headers, body = request_validation_error_handler(exc)
|
|
160
|
-
|
|
161
|
-
assert status == 422
|
|
162
|
-
import json
|
|
163
|
-
data = json.loads(body)
|
|
164
|
-
assert isinstance(data["detail"], list)
|
|
165
|
-
assert len(data["detail"]) == 1
|
|
166
|
-
assert data["detail"][0]["loc"] == ["body", "email"]
|
|
167
|
-
|
|
168
|
-
def test_response_validation_error_handler(self):
|
|
169
|
-
"""Test response validation error handler."""
|
|
170
|
-
errors = [
|
|
171
|
-
{
|
|
172
|
-
"loc": ["response", "id"],
|
|
173
|
-
"msg": "Field required",
|
|
174
|
-
"type": "missing"
|
|
175
|
-
}
|
|
176
|
-
]
|
|
177
|
-
exc = ResponseValidationError(errors)
|
|
178
|
-
status, headers, body = response_validation_error_handler(exc)
|
|
179
|
-
|
|
180
|
-
assert status == 500
|
|
181
|
-
import json
|
|
182
|
-
data = json.loads(body)
|
|
183
|
-
assert data["detail"] == "Response validation error"
|
|
184
|
-
assert "validation_errors" in data["extra"]
|
|
185
|
-
|
|
186
|
-
def test_generic_exception_handler_production(self):
|
|
187
|
-
"""Test generic exception handler in production mode."""
|
|
188
|
-
exc = ValueError("Something went wrong")
|
|
189
|
-
status, headers, body = generic_exception_handler(exc, debug=False)
|
|
190
|
-
|
|
191
|
-
assert status == 500
|
|
192
|
-
import json
|
|
193
|
-
data = json.loads(body)
|
|
194
|
-
assert data["detail"] == "Internal Server Error"
|
|
195
|
-
# Should not expose details in production
|
|
196
|
-
assert "extra" not in data
|
|
197
|
-
|
|
198
|
-
def test_generic_exception_handler_debug(self):
|
|
199
|
-
"""Test generic exception handler in debug mode returns HTML."""
|
|
200
|
-
# Configure Django settings for ExceptionReporter
|
|
201
|
-
import django
|
|
202
|
-
from django.conf import settings
|
|
203
|
-
if not settings.configured:
|
|
204
|
-
settings.configure(
|
|
205
|
-
DEBUG=True,
|
|
206
|
-
SECRET_KEY='test-secret-key',
|
|
207
|
-
INSTALLED_APPS=[],
|
|
208
|
-
ROOT_URLCONF='',
|
|
209
|
-
)
|
|
210
|
-
django.setup()
|
|
211
|
-
|
|
212
|
-
exc = ValueError("Something went wrong")
|
|
213
|
-
|
|
214
|
-
# Create a mock request dict
|
|
215
|
-
request_dict = {
|
|
216
|
-
"method": "GET",
|
|
217
|
-
"path": "/test",
|
|
218
|
-
"headers": {"user-agent": "test"},
|
|
219
|
-
"query_params": {}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
status, headers, body = generic_exception_handler(exc, debug=True, request=request_dict)
|
|
223
|
-
|
|
224
|
-
assert status == 500, "Debug exception must return 500 status"
|
|
225
|
-
# Should return HTML in debug mode
|
|
226
|
-
headers_dict = dict(headers)
|
|
227
|
-
assert headers_dict.get("content-type") == "text/html; charset=utf-8", \
|
|
228
|
-
"Debug mode must return HTML content type"
|
|
229
|
-
# Verify it's HTML content
|
|
230
|
-
html_content = body.decode("utf-8")
|
|
231
|
-
assert "<!DOCTYPE html>" in html_content or "<html>" in html_content, \
|
|
232
|
-
"Debug mode must return valid HTML document"
|
|
233
|
-
assert "ValueError" in html_content, \
|
|
234
|
-
"HTML must contain exception type"
|
|
235
|
-
assert "Something went wrong" in html_content, \
|
|
236
|
-
"HTML must contain exception message"
|
|
237
|
-
|
|
238
|
-
def test_generic_exception_handler_debug_without_request(self):
|
|
239
|
-
"""Test generic exception handler in debug mode works without request."""
|
|
240
|
-
import django
|
|
241
|
-
from django.conf import settings
|
|
242
|
-
if not settings.configured:
|
|
243
|
-
settings.configure(
|
|
244
|
-
DEBUG=True,
|
|
245
|
-
SECRET_KEY='test-secret-key',
|
|
246
|
-
INSTALLED_APPS=[],
|
|
247
|
-
ROOT_URLCONF='',
|
|
248
|
-
)
|
|
249
|
-
django.setup()
|
|
250
|
-
|
|
251
|
-
exc = RuntimeError("Error without request context")
|
|
252
|
-
|
|
253
|
-
# Call without request parameter
|
|
254
|
-
status, headers, body = generic_exception_handler(exc, debug=True, request=None)
|
|
255
|
-
|
|
256
|
-
assert status == 500
|
|
257
|
-
headers_dict = dict(headers)
|
|
258
|
-
assert headers_dict.get("content-type") == "text/html; charset=utf-8"
|
|
259
|
-
html_content = body.decode("utf-8")
|
|
260
|
-
assert "RuntimeError" in html_content
|
|
261
|
-
assert "Error without request context" in html_content
|
|
262
|
-
|
|
263
|
-
def test_generic_exception_handler_debug_fallback_to_json(self):
|
|
264
|
-
"""Test generic exception handler falls back to JSON if HTML generation fails."""
|
|
265
|
-
# Mock ExceptionReporter to raise an exception
|
|
266
|
-
from unittest.mock import patch
|
|
267
|
-
|
|
268
|
-
exc = ValueError("Test exception")
|
|
269
|
-
|
|
270
|
-
# Mock the import inside generic_exception_handler
|
|
271
|
-
with patch('django.views.debug.ExceptionReporter', side_effect=Exception("HTML failed")):
|
|
272
|
-
status, headers, body = generic_exception_handler(exc, debug=True, request=None)
|
|
273
|
-
|
|
274
|
-
assert status == 500, "Fallback must return 500 status"
|
|
275
|
-
headers_dict = dict(headers)
|
|
276
|
-
assert headers_dict.get("content-type") == "application/json", \
|
|
277
|
-
"Fallback must return JSON content type"
|
|
278
|
-
|
|
279
|
-
# Should fall back to JSON with traceback
|
|
280
|
-
import json
|
|
281
|
-
data = json.loads(body)
|
|
282
|
-
assert "ValueError" in data["detail"], \
|
|
283
|
-
"Fallback JSON must contain exception type in detail"
|
|
284
|
-
assert "extra" in data, \
|
|
285
|
-
"Fallback JSON must include extra field"
|
|
286
|
-
assert "traceback" in data["extra"], \
|
|
287
|
-
"Fallback JSON must include traceback"
|
|
288
|
-
assert "exception_type" in data["extra"], \
|
|
289
|
-
"Fallback JSON must include exception_type"
|
|
290
|
-
assert data["extra"]["exception_type"] == "ValueError"
|
|
291
|
-
|
|
292
|
-
def test_generic_exception_handler_preserves_traceback(self):
|
|
293
|
-
"""Test that exception traceback is properly preserved and formatted."""
|
|
294
|
-
def inner_function():
|
|
295
|
-
raise ValueError("Inner error")
|
|
296
|
-
|
|
297
|
-
def outer_function():
|
|
298
|
-
inner_function()
|
|
299
|
-
|
|
300
|
-
try:
|
|
301
|
-
outer_function()
|
|
302
|
-
except ValueError as exc:
|
|
303
|
-
status, headers, body = generic_exception_handler(exc, debug=True, request=None)
|
|
304
|
-
|
|
305
|
-
# Try HTML first (primary path)
|
|
306
|
-
headers_dict = dict(headers)
|
|
307
|
-
if headers_dict.get("content-type") == "text/html; charset=utf-8":
|
|
308
|
-
html_content = body.decode("utf-8")
|
|
309
|
-
# HTML should contain traceback info
|
|
310
|
-
assert "inner_function" in html_content or "outer_function" in html_content, \
|
|
311
|
-
"HTML traceback must show function names"
|
|
312
|
-
else:
|
|
313
|
-
# Fallback JSON path
|
|
314
|
-
import json
|
|
315
|
-
data = json.loads(body)
|
|
316
|
-
traceback_lines = data["extra"]["traceback"]
|
|
317
|
-
traceback_str = "".join(traceback_lines)
|
|
318
|
-
assert "inner_function" in traceback_str, \
|
|
319
|
-
"JSON traceback must contain inner_function"
|
|
320
|
-
assert "outer_function" in traceback_str, \
|
|
321
|
-
"JSON traceback must contain outer_function"
|
|
322
|
-
|
|
323
|
-
def test_handle_exception_http_exception(self):
|
|
324
|
-
"""Test main exception handler with HTTPException."""
|
|
325
|
-
exc = NotFound(detail="Resource not found")
|
|
326
|
-
status, headers, body = handle_exception(exc)
|
|
327
|
-
|
|
328
|
-
assert status == 404
|
|
329
|
-
import json
|
|
330
|
-
data = json.loads(body)
|
|
331
|
-
assert data["detail"] == "Resource not found"
|
|
332
|
-
|
|
333
|
-
def test_handle_exception_validation_error(self):
|
|
334
|
-
"""Test main exception handler with validation error."""
|
|
335
|
-
errors = [{"loc": ["body"], "msg": "Invalid", "type": "value_error"}]
|
|
336
|
-
exc = RequestValidationError(errors)
|
|
337
|
-
status, headers, body = handle_exception(exc)
|
|
338
|
-
|
|
339
|
-
assert status == 422
|
|
340
|
-
import json
|
|
341
|
-
data = json.loads(body)
|
|
342
|
-
assert isinstance(data["detail"], list)
|
|
343
|
-
|
|
344
|
-
def test_handle_exception_generic(self):
|
|
345
|
-
"""Test main exception handler with generic exception."""
|
|
346
|
-
exc = RuntimeError("Unexpected error")
|
|
347
|
-
status, headers, body = handle_exception(exc, debug=False)
|
|
348
|
-
|
|
349
|
-
assert status == 500
|
|
350
|
-
import json
|
|
351
|
-
data = json.loads(body)
|
|
352
|
-
assert data["detail"] == "Internal Server Error"
|
|
353
|
-
|
|
354
|
-
def test_handle_exception_with_request_parameter(self):
|
|
355
|
-
"""Test that handle_exception properly passes request to generic_exception_handler."""
|
|
356
|
-
import django
|
|
357
|
-
from django.conf import settings
|
|
358
|
-
if not settings.configured:
|
|
359
|
-
settings.configure(
|
|
360
|
-
DEBUG=True,
|
|
361
|
-
SECRET_KEY='test-secret-key',
|
|
362
|
-
INSTALLED_APPS=[],
|
|
363
|
-
ROOT_URLCONF='',
|
|
364
|
-
)
|
|
365
|
-
django.setup()
|
|
366
|
-
|
|
367
|
-
exc = ValueError("Test with request")
|
|
368
|
-
request_dict = {
|
|
369
|
-
"method": "POST",
|
|
370
|
-
"path": "/api/users",
|
|
371
|
-
"headers": {"content-type": "application/json"},
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
status, headers, body = handle_exception(exc, debug=True, request=request_dict)
|
|
375
|
-
|
|
376
|
-
assert status == 500, "Exception with request must return 500"
|
|
377
|
-
# Should return HTML in debug mode
|
|
378
|
-
headers_dict = dict(headers)
|
|
379
|
-
assert headers_dict.get("content-type") == "text/html; charset=utf-8", \
|
|
380
|
-
"Debug mode with request must return HTML"
|
|
381
|
-
html_content = body.decode("utf-8")
|
|
382
|
-
assert "ValueError" in html_content
|
|
383
|
-
assert "Test with request" in html_content
|
|
384
|
-
|
|
385
|
-
def test_handle_exception_respects_django_debug_setting(self):
|
|
386
|
-
"""Test that handle_exception uses Django DEBUG setting when debug param is not provided."""
|
|
387
|
-
from django.conf import settings
|
|
388
|
-
|
|
389
|
-
# Store original DEBUG setting
|
|
390
|
-
original_debug = settings.DEBUG
|
|
391
|
-
|
|
392
|
-
# Temporarily set DEBUG to True for this test
|
|
393
|
-
settings.DEBUG = True
|
|
394
|
-
|
|
395
|
-
try:
|
|
396
|
-
exc = ValueError("Should use Django DEBUG")
|
|
397
|
-
|
|
398
|
-
# Call without debug parameter (should check Django settings)
|
|
399
|
-
status, headers, _ = handle_exception(exc)
|
|
400
|
-
|
|
401
|
-
# Since Django DEBUG=True, should return HTML
|
|
402
|
-
headers_dict = dict(headers)
|
|
403
|
-
assert headers_dict.get("content-type") == "text/html; charset=utf-8", \
|
|
404
|
-
"Should use Django DEBUG=True setting when debug param is not provided"
|
|
405
|
-
assert status == 500
|
|
406
|
-
finally:
|
|
407
|
-
# Restore original setting
|
|
408
|
-
settings.DEBUG = original_debug
|
|
409
|
-
|
|
410
|
-
def test_handle_exception_debug_overrides_django_setting(self):
|
|
411
|
-
"""Test that explicit debug=True overrides Django DEBUG=False."""
|
|
412
|
-
exc = ValueError("Explicit debug override")
|
|
413
|
-
|
|
414
|
-
# Explicitly pass debug=True (should ignore Django settings)
|
|
415
|
-
status, _, _ = handle_exception(exc, debug=True)
|
|
416
|
-
|
|
417
|
-
# Should return 500 because we have an exception
|
|
418
|
-
assert status == 500, "Exception must return 500 status"
|
|
419
|
-
|
|
420
|
-
def test_msgspec_validation_error_conversion(self):
|
|
421
|
-
"""Test msgspec ValidationError to dict conversion."""
|
|
422
|
-
# Create a msgspec validation error
|
|
423
|
-
class TestStruct(msgspec.Struct):
|
|
424
|
-
name: str
|
|
425
|
-
age: int
|
|
426
|
-
|
|
427
|
-
try:
|
|
428
|
-
msgspec.json.decode(b'{"age": "invalid"}', type=TestStruct)
|
|
429
|
-
except msgspec.ValidationError as e:
|
|
430
|
-
errors = msgspec_validation_error_to_dict(e)
|
|
431
|
-
assert isinstance(errors, list)
|
|
432
|
-
assert len(errors) > 0
|
|
433
|
-
assert "loc" in errors[0]
|
|
434
|
-
assert "msg" in errors[0]
|
|
435
|
-
assert "type" in errors[0]
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
class TestExceptionIntegration:
|
|
439
|
-
"""Integration tests for exception handling."""
|
|
440
|
-
|
|
441
|
-
def test_exception_chain(self):
|
|
442
|
-
"""Test that exceptions can be chained properly."""
|
|
443
|
-
try:
|
|
444
|
-
raise ValueError("Original error")
|
|
445
|
-
except ValueError as e:
|
|
446
|
-
exc = InternalServerError(detail=str(e))
|
|
447
|
-
assert exc.status_code == 500
|
|
448
|
-
assert "Original error" in exc.detail
|
|
449
|
-
|
|
450
|
-
def test_exception_context_preservation(self):
|
|
451
|
-
"""Test that exception context is preserved."""
|
|
452
|
-
exc = BadRequest(
|
|
453
|
-
detail="Invalid input",
|
|
454
|
-
extra={"field": "email", "value": "invalid@"}
|
|
455
|
-
)
|
|
456
|
-
|
|
457
|
-
status, headers, body = http_exception_handler(exc)
|
|
458
|
-
import json
|
|
459
|
-
data = json.loads(body)
|
|
460
|
-
|
|
461
|
-
assert data["detail"] == "Invalid input"
|
|
462
|
-
assert data["extra"]["field"] == "email"
|
|
463
|
-
assert data["extra"]["value"] == "invalid@"
|
|
464
|
-
|
|
465
|
-
def test_multiple_validation_errors(self):
|
|
466
|
-
"""Test handling multiple validation errors."""
|
|
467
|
-
errors = [
|
|
468
|
-
{"loc": ["body", "name"], "msg": "Field required", "type": "missing"},
|
|
469
|
-
{"loc": ["body", "email"], "msg": "Invalid format", "type": "value_error"},
|
|
470
|
-
{"loc": ["body", "age"], "msg": "Must be positive", "type": "value_error"},
|
|
471
|
-
]
|
|
472
|
-
exc = RequestValidationError(errors)
|
|
473
|
-
status, headers, body = request_validation_error_handler(exc)
|
|
474
|
-
|
|
475
|
-
import json
|
|
476
|
-
data = json.loads(body)
|
|
477
|
-
assert len(data["detail"]) == 3
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if __name__ == "__main__":
|
|
481
|
-
pytest.main([__file__, "-v"])
|