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,536 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Comprehensive tests for JSON parsing and msgspec validation in production/debug modes.
|
|
3
|
-
|
|
4
|
-
Tests cover:
|
|
5
|
-
- Invalid JSON body parsing
|
|
6
|
-
- msgspec struct validation failures
|
|
7
|
-
- Error responses in production vs debug mode
|
|
8
|
-
- Request body validation errors
|
|
9
|
-
- Type coercion and conversion
|
|
10
|
-
"""
|
|
11
|
-
import pytest
|
|
12
|
-
import msgspec
|
|
13
|
-
from django_bolt import BoltAPI
|
|
14
|
-
from django_bolt.exceptions import RequestValidationError
|
|
15
|
-
from django_bolt.error_handlers import handle_exception
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class UserCreate(msgspec.Struct):
|
|
19
|
-
"""Test user creation struct."""
|
|
20
|
-
name: str
|
|
21
|
-
email: str
|
|
22
|
-
age: int
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class UserWithDefaults(msgspec.Struct):
|
|
26
|
-
"""Test struct with default values."""
|
|
27
|
-
name: str
|
|
28
|
-
email: str = "user@example.com"
|
|
29
|
-
is_active: bool = True
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class NestedAddress(msgspec.Struct):
|
|
33
|
-
"""Nested struct for testing."""
|
|
34
|
-
street: str
|
|
35
|
-
city: str
|
|
36
|
-
zipcode: str
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class UserWithNested(msgspec.Struct):
|
|
40
|
-
"""Struct with nested struct."""
|
|
41
|
-
name: str
|
|
42
|
-
address: NestedAddress
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class TestInvalidJSONParsing:
|
|
46
|
-
"""Test invalid JSON body parsing and error handling."""
|
|
47
|
-
|
|
48
|
-
def test_invalid_json_syntax_returns_422(self):
|
|
49
|
-
"""Test that malformed JSON returns 422 with proper error."""
|
|
50
|
-
from django_bolt.binding import create_body_extractor
|
|
51
|
-
|
|
52
|
-
extractor = create_body_extractor("user", UserCreate)
|
|
53
|
-
|
|
54
|
-
# Invalid JSON syntax
|
|
55
|
-
invalid_json = b'{name: "test", email: "test@example.com"}' # Missing quotes
|
|
56
|
-
|
|
57
|
-
# Should convert DecodeError to RequestValidationError (422)
|
|
58
|
-
with pytest.raises(RequestValidationError) as exc_info:
|
|
59
|
-
extractor(invalid_json)
|
|
60
|
-
|
|
61
|
-
errors = exc_info.value.errors()
|
|
62
|
-
assert len(errors) == 1
|
|
63
|
-
assert errors[0]["type"] == "json_invalid"
|
|
64
|
-
# loc is a tuple: ("body", line_num, col_num) when byte position is available
|
|
65
|
-
assert errors[0]["loc"][0] == "body"
|
|
66
|
-
assert "malformed" in errors[0]["msg"].lower() or "keys must be strings" in errors[0]["msg"].lower()
|
|
67
|
-
|
|
68
|
-
def test_empty_json_body_returns_422(self):
|
|
69
|
-
"""Test that empty JSON body returns 422."""
|
|
70
|
-
from django_bolt.binding import create_body_extractor
|
|
71
|
-
|
|
72
|
-
extractor = create_body_extractor("user", UserCreate)
|
|
73
|
-
|
|
74
|
-
# Should convert DecodeError to RequestValidationError (422)
|
|
75
|
-
with pytest.raises(RequestValidationError) as exc_info:
|
|
76
|
-
extractor(b'')
|
|
77
|
-
|
|
78
|
-
errors = exc_info.value.errors()
|
|
79
|
-
assert len(errors) == 1
|
|
80
|
-
assert errors[0]["type"] == "json_invalid"
|
|
81
|
-
# loc is a tuple: ("body",) when no byte position is available
|
|
82
|
-
assert errors[0]["loc"] == ("body",)
|
|
83
|
-
assert "truncated" in errors[0]["msg"].lower()
|
|
84
|
-
|
|
85
|
-
def test_non_json_content_returns_422(self):
|
|
86
|
-
"""Test that non-JSON content returns 422."""
|
|
87
|
-
from django_bolt.binding import create_body_extractor
|
|
88
|
-
|
|
89
|
-
extractor = create_body_extractor("user", UserCreate)
|
|
90
|
-
|
|
91
|
-
# Plain text instead of JSON
|
|
92
|
-
# Should convert DecodeError to RequestValidationError (422)
|
|
93
|
-
with pytest.raises(RequestValidationError) as exc_info:
|
|
94
|
-
extractor(b'this is not json')
|
|
95
|
-
|
|
96
|
-
errors = exc_info.value.errors()
|
|
97
|
-
assert len(errors) == 1
|
|
98
|
-
assert errors[0]["type"] == "json_invalid"
|
|
99
|
-
# loc is a tuple: ("body", line_num, col_num) when byte position is available
|
|
100
|
-
assert errors[0]["loc"][0] == "body"
|
|
101
|
-
assert "malformed" in errors[0]["msg"].lower() or "invalid" in errors[0]["msg"].lower()
|
|
102
|
-
|
|
103
|
-
def test_invalid_json_object_type(self):
|
|
104
|
-
"""Test that JSON with wrong root type fails validation."""
|
|
105
|
-
from django_bolt.binding import create_body_extractor
|
|
106
|
-
|
|
107
|
-
extractor = create_body_extractor("user", UserCreate)
|
|
108
|
-
|
|
109
|
-
# Array instead of object
|
|
110
|
-
with pytest.raises(msgspec.ValidationError):
|
|
111
|
-
extractor(b'["name", "email"]')
|
|
112
|
-
|
|
113
|
-
# String instead of object
|
|
114
|
-
with pytest.raises(msgspec.ValidationError):
|
|
115
|
-
extractor(b'"just a string"')
|
|
116
|
-
|
|
117
|
-
# Number instead of object
|
|
118
|
-
with pytest.raises(msgspec.ValidationError):
|
|
119
|
-
extractor(b'42')
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
class TestMsgspecStructValidation:
|
|
123
|
-
"""Test msgspec struct validation failures."""
|
|
124
|
-
|
|
125
|
-
def test_missing_required_field(self):
|
|
126
|
-
"""Test that missing required field raises ValidationError."""
|
|
127
|
-
from django_bolt.binding import create_body_extractor
|
|
128
|
-
|
|
129
|
-
extractor = create_body_extractor("user", UserCreate)
|
|
130
|
-
|
|
131
|
-
# Missing 'age' field
|
|
132
|
-
with pytest.raises(msgspec.ValidationError) as exc_info:
|
|
133
|
-
extractor(b'{"name": "John", "email": "john@example.com"}')
|
|
134
|
-
|
|
135
|
-
# Verify error mentions the missing field
|
|
136
|
-
assert "age" in str(exc_info.value).lower() or "required" in str(exc_info.value).lower()
|
|
137
|
-
|
|
138
|
-
def test_wrong_field_type(self):
|
|
139
|
-
"""Test that wrong field type raises ValidationError."""
|
|
140
|
-
from django_bolt.binding import create_body_extractor
|
|
141
|
-
|
|
142
|
-
extractor = create_body_extractor("user", UserCreate)
|
|
143
|
-
|
|
144
|
-
# age should be int, not string
|
|
145
|
-
with pytest.raises(msgspec.ValidationError):
|
|
146
|
-
extractor(b'{"name": "John", "email": "john@example.com", "age": "twenty"}')
|
|
147
|
-
|
|
148
|
-
# name should be string, not number
|
|
149
|
-
with pytest.raises(msgspec.ValidationError):
|
|
150
|
-
extractor(b'{"name": 123, "email": "john@example.com", "age": 20}')
|
|
151
|
-
|
|
152
|
-
def test_null_for_required_field(self):
|
|
153
|
-
"""Test that null for required field raises ValidationError."""
|
|
154
|
-
from django_bolt.binding import create_body_extractor
|
|
155
|
-
|
|
156
|
-
extractor = create_body_extractor("user", UserCreate)
|
|
157
|
-
|
|
158
|
-
with pytest.raises(msgspec.ValidationError):
|
|
159
|
-
extractor(b'{"name": null, "email": "john@example.com", "age": 20}')
|
|
160
|
-
|
|
161
|
-
def test_extra_fields_allowed_by_default(self):
|
|
162
|
-
"""Test that extra fields are allowed by default in msgspec."""
|
|
163
|
-
from django_bolt.binding import create_body_extractor
|
|
164
|
-
|
|
165
|
-
extractor = create_body_extractor("user", UserCreate)
|
|
166
|
-
|
|
167
|
-
# Should succeed even with extra field
|
|
168
|
-
result = extractor(b'{"name": "John", "email": "john@example.com", "age": 20, "extra": "field"}')
|
|
169
|
-
assert result.name == "John"
|
|
170
|
-
assert result.email == "john@example.com"
|
|
171
|
-
assert result.age == 20
|
|
172
|
-
|
|
173
|
-
def test_nested_struct_validation(self):
|
|
174
|
-
"""Test validation of nested structs."""
|
|
175
|
-
from django_bolt.binding import create_body_extractor
|
|
176
|
-
|
|
177
|
-
extractor = create_body_extractor("user", UserWithNested)
|
|
178
|
-
|
|
179
|
-
# Valid nested structure
|
|
180
|
-
valid_json = b'''{
|
|
181
|
-
"name": "John",
|
|
182
|
-
"address": {
|
|
183
|
-
"street": "123 Main St",
|
|
184
|
-
"city": "New York",
|
|
185
|
-
"zipcode": "10001"
|
|
186
|
-
}
|
|
187
|
-
}'''
|
|
188
|
-
result = extractor(valid_json)
|
|
189
|
-
assert result.name == "John"
|
|
190
|
-
assert result.address.city == "New York"
|
|
191
|
-
|
|
192
|
-
# Invalid nested structure (missing city)
|
|
193
|
-
invalid_json = b'''{
|
|
194
|
-
"name": "John",
|
|
195
|
-
"address": {
|
|
196
|
-
"street": "123 Main St",
|
|
197
|
-
"zipcode": "10001"
|
|
198
|
-
}
|
|
199
|
-
}'''
|
|
200
|
-
with pytest.raises(msgspec.ValidationError):
|
|
201
|
-
extractor(invalid_json)
|
|
202
|
-
|
|
203
|
-
def test_array_field_validation(self):
|
|
204
|
-
"""Test validation of array fields."""
|
|
205
|
-
class UserWithTags(msgspec.Struct):
|
|
206
|
-
name: str
|
|
207
|
-
tags: list[str]
|
|
208
|
-
|
|
209
|
-
from django_bolt.binding import create_body_extractor
|
|
210
|
-
extractor = create_body_extractor("user", UserWithTags)
|
|
211
|
-
|
|
212
|
-
# Valid array
|
|
213
|
-
result = extractor(b'{"name": "John", "tags": ["admin", "user"]}')
|
|
214
|
-
assert result.tags == ["admin", "user"]
|
|
215
|
-
|
|
216
|
-
# Invalid array element type
|
|
217
|
-
with pytest.raises(msgspec.ValidationError):
|
|
218
|
-
extractor(b'{"name": "John", "tags": ["admin", 123]}')
|
|
219
|
-
|
|
220
|
-
# Array instead of string element
|
|
221
|
-
with pytest.raises(msgspec.ValidationError):
|
|
222
|
-
extractor(b'{"name": "John", "tags": [["nested"]]}')
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
class TestProductionVsDebugMode:
|
|
226
|
-
"""Test error responses in production vs debug mode."""
|
|
227
|
-
|
|
228
|
-
def test_validation_error_in_production_mode(self):
|
|
229
|
-
"""Test that validation errors return 422 in production without stack traces."""
|
|
230
|
-
# Simulate validation error
|
|
231
|
-
exc = msgspec.ValidationError("Expected int, got str")
|
|
232
|
-
|
|
233
|
-
# Handle in production mode (debug=False)
|
|
234
|
-
status, headers, body = handle_exception(exc, debug=False)
|
|
235
|
-
|
|
236
|
-
assert status == 422, "Validation error must return 422"
|
|
237
|
-
|
|
238
|
-
# Parse response
|
|
239
|
-
import json
|
|
240
|
-
data = json.loads(body)
|
|
241
|
-
|
|
242
|
-
# Should have validation errors
|
|
243
|
-
assert "detail" in data
|
|
244
|
-
assert isinstance(data["detail"], list), "Validation errors should be a list"
|
|
245
|
-
|
|
246
|
-
# Should NOT have stack traces in production
|
|
247
|
-
if "extra" in data:
|
|
248
|
-
assert "traceback" not in data["extra"], "Production mode should not expose traceback"
|
|
249
|
-
|
|
250
|
-
def test_validation_error_in_debug_mode(self):
|
|
251
|
-
"""Test that validation errors in debug mode may include more details."""
|
|
252
|
-
# Simulate validation error
|
|
253
|
-
exc = msgspec.ValidationError("Expected int, got str")
|
|
254
|
-
|
|
255
|
-
# Handle in debug mode (debug=True)
|
|
256
|
-
status, headers, body = handle_exception(exc, debug=True)
|
|
257
|
-
|
|
258
|
-
assert status == 422, "Validation error must return 422 even in debug"
|
|
259
|
-
|
|
260
|
-
# Response should still be JSON (not HTML for validation errors)
|
|
261
|
-
headers_dict = dict(headers)
|
|
262
|
-
assert headers_dict.get("content-type") == "application/json"
|
|
263
|
-
|
|
264
|
-
def test_generic_exception_differs_by_mode(self):
|
|
265
|
-
"""Test that generic exceptions are handled differently in prod vs debug."""
|
|
266
|
-
exc = ValueError("Something went wrong")
|
|
267
|
-
|
|
268
|
-
# Production mode - should hide details
|
|
269
|
-
prod_status, prod_headers, prod_body = handle_exception(exc, debug=False)
|
|
270
|
-
assert prod_status == 500
|
|
271
|
-
|
|
272
|
-
import json
|
|
273
|
-
prod_data = json.loads(prod_body)
|
|
274
|
-
assert prod_data["detail"] == "Internal Server Error", \
|
|
275
|
-
"Production should hide error details"
|
|
276
|
-
assert "extra" not in prod_data, \
|
|
277
|
-
"Production should not expose exception details"
|
|
278
|
-
|
|
279
|
-
# Debug mode - should show details (HTML or JSON with traceback)
|
|
280
|
-
debug_status, debug_headers, debug_body = handle_exception(exc, debug=True)
|
|
281
|
-
assert debug_status == 500
|
|
282
|
-
|
|
283
|
-
# Debug mode returns either HTML or JSON with traceback
|
|
284
|
-
debug_headers_dict = dict(debug_headers)
|
|
285
|
-
if debug_headers_dict.get("content-type") == "text/html; charset=utf-8":
|
|
286
|
-
# HTML error page
|
|
287
|
-
html = debug_body.decode()
|
|
288
|
-
assert "ValueError" in html
|
|
289
|
-
else:
|
|
290
|
-
# JSON with traceback
|
|
291
|
-
debug_data = json.loads(debug_body)
|
|
292
|
-
assert "extra" in debug_data
|
|
293
|
-
assert "traceback" in debug_data["extra"]
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
class TestRequestValidationErrorHandling:
|
|
297
|
-
"""Test RequestValidationError handling."""
|
|
298
|
-
|
|
299
|
-
def test_request_validation_error_format(self):
|
|
300
|
-
"""Test that RequestValidationError returns proper format."""
|
|
301
|
-
errors = [
|
|
302
|
-
{
|
|
303
|
-
"loc": ["body", "email"],
|
|
304
|
-
"msg": "Invalid email format",
|
|
305
|
-
"type": "value_error"
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
"loc": ["body", "age"],
|
|
309
|
-
"msg": "Must be a positive integer",
|
|
310
|
-
"type": "value_error"
|
|
311
|
-
}
|
|
312
|
-
]
|
|
313
|
-
|
|
314
|
-
exc = RequestValidationError(errors)
|
|
315
|
-
status, headers, body = handle_exception(exc, debug=False)
|
|
316
|
-
|
|
317
|
-
assert status == 422
|
|
318
|
-
|
|
319
|
-
import json
|
|
320
|
-
data = json.loads(body)
|
|
321
|
-
|
|
322
|
-
# Should return errors in detail field
|
|
323
|
-
assert "detail" in data
|
|
324
|
-
assert isinstance(data["detail"], list)
|
|
325
|
-
assert len(data["detail"]) == 2
|
|
326
|
-
|
|
327
|
-
# Each error should have loc, msg, type
|
|
328
|
-
for error in data["detail"]:
|
|
329
|
-
assert "loc" in error
|
|
330
|
-
assert "msg" in error
|
|
331
|
-
assert "type" in error
|
|
332
|
-
|
|
333
|
-
def test_request_validation_error_with_body(self):
|
|
334
|
-
"""Test RequestValidationError preserves request body for debugging."""
|
|
335
|
-
errors = [{"loc": ["body", "name"], "msg": "Field required", "type": "missing"}]
|
|
336
|
-
body = {"email": "test@example.com"} # Missing 'name'
|
|
337
|
-
|
|
338
|
-
exc = RequestValidationError(errors, body=body)
|
|
339
|
-
|
|
340
|
-
# Error should store the body
|
|
341
|
-
assert exc.body == body
|
|
342
|
-
|
|
343
|
-
def test_msgspec_error_to_request_validation_error(self):
|
|
344
|
-
"""Test that msgspec.ValidationError is converted properly."""
|
|
345
|
-
from django_bolt.error_handlers import msgspec_validation_error_to_dict
|
|
346
|
-
|
|
347
|
-
# Create a validation error
|
|
348
|
-
class TestStruct(msgspec.Struct):
|
|
349
|
-
name: str
|
|
350
|
-
age: int
|
|
351
|
-
|
|
352
|
-
try:
|
|
353
|
-
msgspec.json.decode(b'{"name": "John", "age": "invalid"}', type=TestStruct)
|
|
354
|
-
except msgspec.ValidationError as e:
|
|
355
|
-
errors = msgspec_validation_error_to_dict(e)
|
|
356
|
-
|
|
357
|
-
assert isinstance(errors, list)
|
|
358
|
-
assert len(errors) > 0
|
|
359
|
-
|
|
360
|
-
# Each error should have required fields
|
|
361
|
-
for error in errors:
|
|
362
|
-
assert "loc" in error
|
|
363
|
-
assert "msg" in error
|
|
364
|
-
assert "type" in error
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
class TestTypeCoercionEdgeCases:
|
|
368
|
-
"""Test edge cases in type coercion and conversion."""
|
|
369
|
-
|
|
370
|
-
def test_boolean_coercion_from_string(self):
|
|
371
|
-
"""Test boolean coercion from string query params."""
|
|
372
|
-
from django_bolt.binding import convert_primitive
|
|
373
|
-
|
|
374
|
-
# True values
|
|
375
|
-
assert convert_primitive("true", bool) is True
|
|
376
|
-
assert convert_primitive("True", bool) is True
|
|
377
|
-
assert convert_primitive("1", bool) is True
|
|
378
|
-
assert convert_primitive("yes", bool) is True
|
|
379
|
-
assert convert_primitive("on", bool) is True
|
|
380
|
-
|
|
381
|
-
# False values
|
|
382
|
-
assert convert_primitive("false", bool) is False
|
|
383
|
-
assert convert_primitive("False", bool) is False
|
|
384
|
-
assert convert_primitive("0", bool) is False
|
|
385
|
-
assert convert_primitive("no", bool) is False
|
|
386
|
-
assert convert_primitive("off", bool) is False
|
|
387
|
-
|
|
388
|
-
def test_number_coercion_errors(self):
|
|
389
|
-
"""Test that invalid number strings raise errors."""
|
|
390
|
-
from django_bolt.binding import convert_primitive
|
|
391
|
-
from django_bolt.exceptions import HTTPException
|
|
392
|
-
|
|
393
|
-
# Invalid int - now raises HTTPException(422) instead of ValueError
|
|
394
|
-
with pytest.raises(HTTPException) as exc_info:
|
|
395
|
-
convert_primitive("not_a_number", int)
|
|
396
|
-
assert exc_info.value.status_code == 422
|
|
397
|
-
|
|
398
|
-
# Invalid float - now raises HTTPException(422) instead of ValueError
|
|
399
|
-
with pytest.raises(HTTPException) as exc_info:
|
|
400
|
-
convert_primitive("not_a_float", float)
|
|
401
|
-
assert exc_info.value.status_code == 422
|
|
402
|
-
|
|
403
|
-
def test_empty_string_coercion(self):
|
|
404
|
-
"""Test coercion of empty strings."""
|
|
405
|
-
from django_bolt.binding import convert_primitive
|
|
406
|
-
from django_bolt.exceptions import HTTPException
|
|
407
|
-
|
|
408
|
-
# Empty string for string type should be empty string
|
|
409
|
-
assert convert_primitive("", str) == ""
|
|
410
|
-
|
|
411
|
-
# Empty string for int should fail - now raises HTTPException(422)
|
|
412
|
-
with pytest.raises(HTTPException) as exc_info:
|
|
413
|
-
convert_primitive("", int)
|
|
414
|
-
assert exc_info.value.status_code == 422
|
|
415
|
-
|
|
416
|
-
def test_optional_fields_with_none(self):
|
|
417
|
-
"""Test that optional fields handle None correctly."""
|
|
418
|
-
class UserOptional(msgspec.Struct):
|
|
419
|
-
name: str
|
|
420
|
-
email: str | None = None
|
|
421
|
-
|
|
422
|
-
from django_bolt.binding import create_body_extractor
|
|
423
|
-
extractor = create_body_extractor("user", UserOptional)
|
|
424
|
-
|
|
425
|
-
# Explicit null
|
|
426
|
-
result = extractor(b'{"name": "John", "email": null}')
|
|
427
|
-
assert result.name == "John"
|
|
428
|
-
assert result.email is None
|
|
429
|
-
|
|
430
|
-
# Missing optional field
|
|
431
|
-
result = extractor(b'{"name": "John"}')
|
|
432
|
-
assert result.name == "John"
|
|
433
|
-
assert result.email is None
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
class TestJSONParsingPerformance:
|
|
437
|
-
"""Test JSON parsing performance characteristics."""
|
|
438
|
-
|
|
439
|
-
def test_decoder_caching(self):
|
|
440
|
-
"""Test that msgspec decoders are cached for performance."""
|
|
441
|
-
from django_bolt.binding import get_msgspec_decoder, _DECODER_CACHE
|
|
442
|
-
|
|
443
|
-
# Clear cache
|
|
444
|
-
_DECODER_CACHE.clear()
|
|
445
|
-
|
|
446
|
-
# First call should create decoder
|
|
447
|
-
decoder1 = get_msgspec_decoder(UserCreate)
|
|
448
|
-
assert UserCreate in _DECODER_CACHE
|
|
449
|
-
|
|
450
|
-
# Second call should return cached decoder
|
|
451
|
-
decoder2 = get_msgspec_decoder(UserCreate)
|
|
452
|
-
assert decoder1 is decoder2, "Decoder should be cached"
|
|
453
|
-
|
|
454
|
-
def test_large_json_parsing(self):
|
|
455
|
-
"""Test parsing of large JSON payloads."""
|
|
456
|
-
from django_bolt.binding import create_body_extractor
|
|
457
|
-
|
|
458
|
-
class LargeStruct(msgspec.Struct):
|
|
459
|
-
items: list[dict]
|
|
460
|
-
|
|
461
|
-
extractor = create_body_extractor("data", LargeStruct)
|
|
462
|
-
|
|
463
|
-
# Create large JSON with 1000 items
|
|
464
|
-
items = [{"id": i, "name": f"item_{i}"} for i in range(1000)]
|
|
465
|
-
import json
|
|
466
|
-
large_json = json.dumps({"items": items}).encode()
|
|
467
|
-
|
|
468
|
-
# Should parse successfully
|
|
469
|
-
result = extractor(large_json)
|
|
470
|
-
assert len(result.items) == 1000
|
|
471
|
-
|
|
472
|
-
def test_deeply_nested_json(self):
|
|
473
|
-
"""Test parsing of deeply nested JSON structures."""
|
|
474
|
-
class Level3(msgspec.Struct):
|
|
475
|
-
value: str
|
|
476
|
-
|
|
477
|
-
class Level2(msgspec.Struct):
|
|
478
|
-
level3: Level3
|
|
479
|
-
|
|
480
|
-
class Level1(msgspec.Struct):
|
|
481
|
-
level2: Level2
|
|
482
|
-
|
|
483
|
-
from django_bolt.binding import create_body_extractor
|
|
484
|
-
extractor = create_body_extractor("data", Level1)
|
|
485
|
-
|
|
486
|
-
nested_json = b'''{
|
|
487
|
-
"level2": {
|
|
488
|
-
"level3": {
|
|
489
|
-
"value": "deep"
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}'''
|
|
493
|
-
|
|
494
|
-
result = extractor(nested_json)
|
|
495
|
-
assert result.level2.level3.value == "deep"
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
class TestIntegrationWithBoltAPI:
|
|
499
|
-
"""Integration tests with BoltAPI."""
|
|
500
|
-
|
|
501
|
-
def test_api_handles_invalid_json_body(self):
|
|
502
|
-
"""Test that BoltAPI properly handles invalid JSON in request body."""
|
|
503
|
-
api = BoltAPI()
|
|
504
|
-
|
|
505
|
-
@api.post("/users")
|
|
506
|
-
async def create_user(user: UserCreate):
|
|
507
|
-
return {"id": 1, "name": user.name}
|
|
508
|
-
|
|
509
|
-
# The route should be registered
|
|
510
|
-
assert len(api._routes) == 1
|
|
511
|
-
|
|
512
|
-
def test_api_validation_error_response(self):
|
|
513
|
-
"""Test that API returns proper validation error response."""
|
|
514
|
-
api = BoltAPI()
|
|
515
|
-
|
|
516
|
-
@api.post("/users")
|
|
517
|
-
async def create_user(user: UserCreate):
|
|
518
|
-
return {"id": 1, "name": user.name}
|
|
519
|
-
|
|
520
|
-
# Simulate request with missing field
|
|
521
|
-
# This would normally be caught during binding
|
|
522
|
-
# Here we test the error handler behavior
|
|
523
|
-
errors = [{"loc": ["body", "age"], "msg": "Field required", "type": "missing"}]
|
|
524
|
-
exc = RequestValidationError(errors)
|
|
525
|
-
|
|
526
|
-
status, headers, body = handle_exception(exc, debug=False)
|
|
527
|
-
assert status == 422
|
|
528
|
-
|
|
529
|
-
import json
|
|
530
|
-
data = json.loads(body)
|
|
531
|
-
assert len(data["detail"]) == 1
|
|
532
|
-
assert data["detail"][0]["loc"] == ["body", "age"]
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if __name__ == "__main__":
|
|
536
|
-
pytest.main([__file__, "-v", "-s"])
|