django-bolt 0.1.0__cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.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.

Files changed (128) hide show
  1. django_bolt/__init__.py +147 -0
  2. django_bolt/_core.abi3.so +0 -0
  3. django_bolt/admin/__init__.py +25 -0
  4. django_bolt/admin/admin_detection.py +179 -0
  5. django_bolt/admin/asgi_bridge.py +267 -0
  6. django_bolt/admin/routes.py +91 -0
  7. django_bolt/admin/static.py +155 -0
  8. django_bolt/admin/static_routes.py +111 -0
  9. django_bolt/api.py +1011 -0
  10. django_bolt/apps.py +7 -0
  11. django_bolt/async_collector.py +228 -0
  12. django_bolt/auth/README.md +464 -0
  13. django_bolt/auth/REVOCATION_EXAMPLE.md +391 -0
  14. django_bolt/auth/__init__.py +84 -0
  15. django_bolt/auth/backends.py +236 -0
  16. django_bolt/auth/guards.py +224 -0
  17. django_bolt/auth/jwt_utils.py +212 -0
  18. django_bolt/auth/revocation.py +286 -0
  19. django_bolt/auth/token.py +335 -0
  20. django_bolt/binding.py +363 -0
  21. django_bolt/bootstrap.py +77 -0
  22. django_bolt/cli.py +133 -0
  23. django_bolt/compression.py +104 -0
  24. django_bolt/decorators.py +159 -0
  25. django_bolt/dependencies.py +128 -0
  26. django_bolt/error_handlers.py +305 -0
  27. django_bolt/exceptions.py +294 -0
  28. django_bolt/health.py +129 -0
  29. django_bolt/logging/__init__.py +6 -0
  30. django_bolt/logging/config.py +357 -0
  31. django_bolt/logging/middleware.py +296 -0
  32. django_bolt/management/__init__.py +1 -0
  33. django_bolt/management/commands/__init__.py +0 -0
  34. django_bolt/management/commands/runbolt.py +427 -0
  35. django_bolt/middleware/__init__.py +32 -0
  36. django_bolt/middleware/compiler.py +131 -0
  37. django_bolt/middleware/middleware.py +247 -0
  38. django_bolt/openapi/__init__.py +23 -0
  39. django_bolt/openapi/config.py +196 -0
  40. django_bolt/openapi/plugins.py +439 -0
  41. django_bolt/openapi/routes.py +152 -0
  42. django_bolt/openapi/schema_generator.py +581 -0
  43. django_bolt/openapi/spec/__init__.py +68 -0
  44. django_bolt/openapi/spec/base.py +74 -0
  45. django_bolt/openapi/spec/callback.py +24 -0
  46. django_bolt/openapi/spec/components.py +72 -0
  47. django_bolt/openapi/spec/contact.py +21 -0
  48. django_bolt/openapi/spec/discriminator.py +25 -0
  49. django_bolt/openapi/spec/encoding.py +67 -0
  50. django_bolt/openapi/spec/enums.py +41 -0
  51. django_bolt/openapi/spec/example.py +36 -0
  52. django_bolt/openapi/spec/external_documentation.py +21 -0
  53. django_bolt/openapi/spec/header.py +132 -0
  54. django_bolt/openapi/spec/info.py +50 -0
  55. django_bolt/openapi/spec/license.py +28 -0
  56. django_bolt/openapi/spec/link.py +66 -0
  57. django_bolt/openapi/spec/media_type.py +51 -0
  58. django_bolt/openapi/spec/oauth_flow.py +36 -0
  59. django_bolt/openapi/spec/oauth_flows.py +28 -0
  60. django_bolt/openapi/spec/open_api.py +87 -0
  61. django_bolt/openapi/spec/operation.py +105 -0
  62. django_bolt/openapi/spec/parameter.py +147 -0
  63. django_bolt/openapi/spec/path_item.py +78 -0
  64. django_bolt/openapi/spec/paths.py +27 -0
  65. django_bolt/openapi/spec/reference.py +38 -0
  66. django_bolt/openapi/spec/request_body.py +38 -0
  67. django_bolt/openapi/spec/response.py +48 -0
  68. django_bolt/openapi/spec/responses.py +44 -0
  69. django_bolt/openapi/spec/schema.py +678 -0
  70. django_bolt/openapi/spec/security_requirement.py +28 -0
  71. django_bolt/openapi/spec/security_scheme.py +69 -0
  72. django_bolt/openapi/spec/server.py +34 -0
  73. django_bolt/openapi/spec/server_variable.py +32 -0
  74. django_bolt/openapi/spec/tag.py +32 -0
  75. django_bolt/openapi/spec/xml.py +44 -0
  76. django_bolt/pagination.py +669 -0
  77. django_bolt/param_functions.py +49 -0
  78. django_bolt/params.py +337 -0
  79. django_bolt/request_parsing.py +128 -0
  80. django_bolt/responses.py +214 -0
  81. django_bolt/router.py +48 -0
  82. django_bolt/serialization.py +193 -0
  83. django_bolt/status_codes.py +321 -0
  84. django_bolt/testing/__init__.py +10 -0
  85. django_bolt/testing/client.py +274 -0
  86. django_bolt/testing/helpers.py +93 -0
  87. django_bolt/tests/__init__.py +0 -0
  88. django_bolt/tests/admin_tests/__init__.py +1 -0
  89. django_bolt/tests/admin_tests/conftest.py +6 -0
  90. django_bolt/tests/admin_tests/test_admin_with_django.py +278 -0
  91. django_bolt/tests/admin_tests/urls.py +9 -0
  92. django_bolt/tests/cbv/__init__.py +0 -0
  93. django_bolt/tests/cbv/test_class_views.py +570 -0
  94. django_bolt/tests/cbv/test_class_views_django_orm.py +703 -0
  95. django_bolt/tests/cbv/test_class_views_features.py +1173 -0
  96. django_bolt/tests/cbv/test_class_views_with_client.py +622 -0
  97. django_bolt/tests/conftest.py +165 -0
  98. django_bolt/tests/test_action_decorator.py +399 -0
  99. django_bolt/tests/test_auth_secret_key.py +83 -0
  100. django_bolt/tests/test_decorator_syntax.py +159 -0
  101. django_bolt/tests/test_error_handling.py +481 -0
  102. django_bolt/tests/test_file_response.py +192 -0
  103. django_bolt/tests/test_global_cors.py +172 -0
  104. django_bolt/tests/test_guards_auth.py +441 -0
  105. django_bolt/tests/test_guards_integration.py +303 -0
  106. django_bolt/tests/test_health.py +283 -0
  107. django_bolt/tests/test_integration_validation.py +400 -0
  108. django_bolt/tests/test_json_validation.py +536 -0
  109. django_bolt/tests/test_jwt_auth.py +327 -0
  110. django_bolt/tests/test_jwt_token.py +458 -0
  111. django_bolt/tests/test_logging.py +837 -0
  112. django_bolt/tests/test_logging_merge.py +419 -0
  113. django_bolt/tests/test_middleware.py +492 -0
  114. django_bolt/tests/test_middleware_server.py +230 -0
  115. django_bolt/tests/test_model_viewset.py +323 -0
  116. django_bolt/tests/test_models.py +24 -0
  117. django_bolt/tests/test_pagination.py +1258 -0
  118. django_bolt/tests/test_parameter_validation.py +178 -0
  119. django_bolt/tests/test_syntax.py +626 -0
  120. django_bolt/tests/test_testing_utilities.py +163 -0
  121. django_bolt/tests/test_testing_utilities_simple.py +123 -0
  122. django_bolt/tests/test_viewset_unified.py +346 -0
  123. django_bolt/typing.py +273 -0
  124. django_bolt/views.py +1110 -0
  125. django_bolt-0.1.0.dist-info/METADATA +629 -0
  126. django_bolt-0.1.0.dist-info/RECORD +128 -0
  127. django_bolt-0.1.0.dist-info/WHEEL +4 -0
  128. django_bolt-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,536 @@
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"])