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,570 @@
1
+ """
2
+ Tests for class-based views.
3
+
4
+ Tests cover:
5
+ - Basic APIView functionality
6
+ - Parameter extraction and dependency injection
7
+ - Guards and authentication
8
+ - Return type annotations
9
+ - Mixins (ListMixin, RetrieveMixin, CreateMixin, etc.)
10
+ - ViewSet
11
+ """
12
+ import pytest
13
+ import msgspec
14
+ from typing import Dict, Any, List
15
+ from django_bolt import BoltAPI
16
+ from django_bolt.views import (
17
+ APIView,
18
+ ViewSet,
19
+ ListMixin,
20
+ RetrieveMixin,
21
+ CreateMixin,
22
+ UpdateMixin,
23
+ PartialUpdateMixin,
24
+ DestroyMixin,
25
+ )
26
+ from django_bolt.params import Depends
27
+ from django_bolt.exceptions import HTTPException
28
+ from django_bolt.auth.guards import IsAuthenticated
29
+ from django_bolt.auth.backends import JWTAuthentication
30
+
31
+
32
+ # --- Test Fixtures ---
33
+
34
+ @pytest.fixture
35
+ def api():
36
+ """Create a fresh BoltAPI instance for each test."""
37
+ return BoltAPI()
38
+
39
+
40
+ def create_request(
41
+ path_params: Dict[str, Any] = None,
42
+ query_params: Dict[str, Any] = None,
43
+ headers: Dict[str, str] = None,
44
+ body: bytes = b"{}",
45
+ auth: Dict[str, Any] = None,
46
+ ) -> Dict[str, Any]:
47
+ """Helper to create mock request dictionary."""
48
+ return {
49
+ "params": path_params or {},
50
+ "query": query_params or {},
51
+ "headers": headers or {},
52
+ "cookies": {},
53
+ "body": body,
54
+ "auth": auth or {},
55
+ "method": "GET",
56
+ "path": "/",
57
+ }
58
+
59
+
60
+ # --- Basic Tests ---
61
+
62
+
63
+ def test_bolt_api_view_basic(api):
64
+ """Test basic APIView with GET handler."""
65
+
66
+ @api.view("/hello")
67
+ class HelloView(APIView):
68
+ async def get(self, request) -> dict:
69
+ return {"message": "Hello"}
70
+
71
+ # Verify route was registered
72
+ assert len(api._routes) == 1
73
+ method, path, handler_id, handler = api._routes[0]
74
+ assert method == "GET"
75
+ assert path == "/hello"
76
+
77
+
78
+ @pytest.mark.asyncio
79
+ async def test_bolt_api_view_dispatch(api):
80
+ """Test that view handlers are actually called."""
81
+
82
+ @api.view("/hello")
83
+ class HelloView(APIView):
84
+ async def get(self, request) -> dict:
85
+ return {"message": "Hello, World!"}
86
+
87
+ # Get the registered handler
88
+ handler = api._routes[0][3]
89
+ request = create_request()
90
+
91
+ # Dispatch and verify result
92
+ result = await handler(request)
93
+ assert result == {"message": "Hello, World!"}
94
+
95
+
96
+ @pytest.mark.asyncio
97
+ async def test_bolt_api_view_multiple_methods(api):
98
+ """Test view with multiple HTTP methods."""
99
+
100
+ @api.view("/multi")
101
+ class MultiMethodView(APIView):
102
+ async def get(self, request) -> dict:
103
+ return {"method": "GET"}
104
+
105
+ async def post(self, request) -> dict:
106
+ return {"method": "POST"}
107
+
108
+ async def put(self, request) -> dict:
109
+ return {"method": "PUT"}
110
+
111
+ # Verify all methods registered
112
+ assert len(api._routes) == 3
113
+ methods = {route[0] for route in api._routes}
114
+ assert methods == {"GET", "POST", "PUT"}
115
+
116
+
117
+ @pytest.mark.asyncio
118
+ async def test_bolt_api_view_path_params(api):
119
+ """Test path parameter extraction in class-based views."""
120
+
121
+ @api.view("/users/{user_id}")
122
+ class UserView(APIView):
123
+ async def get(self, request, user_id: int) -> dict:
124
+ return {"user_id": user_id, "type": type(user_id).__name__}
125
+
126
+ # Get handler and test with path param
127
+ handler = api._routes[0][3]
128
+ request = create_request(path_params={"user_id": "123"})
129
+
130
+ result = await handler(request, user_id=123) # Rust passes as int
131
+ assert result["user_id"] == 123
132
+ assert result["type"] == "int"
133
+
134
+
135
+ @pytest.mark.asyncio
136
+ async def test_bolt_api_view_query_params(api):
137
+ """Test query parameter extraction in class-based views."""
138
+
139
+ @api.view("/search")
140
+ class SearchView(APIView):
141
+ async def get(self, request, q: str, limit: int = 10) -> dict:
142
+ return {"query": q, "limit": limit}
143
+
144
+ handler = api._routes[0][3]
145
+
146
+ # Test with both params
147
+ request = create_request(query_params={"q": "test", "limit": "20"})
148
+ result = await handler(request, q="test", limit=20)
149
+ assert result == {"query": "test", "limit": 20}
150
+
151
+
152
+ @pytest.mark.asyncio
153
+ async def test_bolt_api_view_dependency_injection(api):
154
+ """Test dependency injection in class-based views."""
155
+
156
+ async def get_current_user(request) -> dict:
157
+ return {"id": 1, "username": "testuser"}
158
+
159
+ @api.view("/profile")
160
+ class ProfileView(APIView):
161
+ async def get(self, request, current_user=Depends(get_current_user)) -> dict:
162
+ return {"user": current_user}
163
+
164
+ # This test verifies the handler signature is preserved
165
+ handler = api._routes[0][3]
166
+
167
+ # Check that handler has correct signature
168
+ import inspect
169
+ sig = inspect.signature(handler)
170
+ assert "current_user" in sig.parameters
171
+
172
+
173
+ @pytest.mark.asyncio
174
+ async def test_bolt_api_view_request_body(api):
175
+ """Test request body parsing with msgspec.Struct."""
176
+
177
+ class CreateUserRequest(msgspec.Struct):
178
+ username: str
179
+ email: str
180
+
181
+ @api.view("/users")
182
+ class UserCreateView(APIView):
183
+ async def post(self, request, data: CreateUserRequest) -> dict:
184
+ return {"username": data.username, "email": data.email}
185
+
186
+ handler = api._routes[0][3]
187
+
188
+ # Verify handler signature includes data parameter
189
+ import inspect
190
+ sig = inspect.signature(handler)
191
+ assert "data" in sig.parameters
192
+
193
+
194
+ @pytest.mark.asyncio
195
+ async def test_bolt_api_view_return_annotation(api):
196
+ """Test that return annotations are preserved."""
197
+
198
+ class ResponseSchema(msgspec.Struct):
199
+ message: str
200
+ count: int
201
+
202
+ @api.view("/annotated")
203
+ class AnnotatedView(APIView):
204
+ async def get(self, request) -> ResponseSchema:
205
+ return ResponseSchema(message="test", count=42)
206
+
207
+ # Check that handler signature includes return annotation
208
+ handler = api._routes[0][3]
209
+ import inspect
210
+ sig = inspect.signature(handler)
211
+ assert sig.return_annotation == ResponseSchema
212
+
213
+
214
+ # --- Guards and Authentication Tests ---
215
+
216
+
217
+ def test_bolt_api_view_class_level_guards(api):
218
+ """Test class-level guards are applied."""
219
+
220
+ @api.view("/protected")
221
+ class ProtectedView(APIView):
222
+ guards = [IsAuthenticated()]
223
+
224
+ async def get(self, request) -> dict:
225
+ return {"protected": True}
226
+
227
+ # Verify middleware metadata includes guards
228
+ handler_id = api._routes[0][2]
229
+ middleware_meta = api._handler_middleware.get(handler_id)
230
+ assert middleware_meta is not None
231
+ assert "guards" in middleware_meta
232
+ assert len(middleware_meta["guards"]) == 1
233
+
234
+
235
+ def test_bolt_api_view_route_level_guard_override(api):
236
+ """Test route-level guards override class-level guards."""
237
+ from django_bolt.auth.guards import IsAdminUser
238
+
239
+ # Override with route-level guards
240
+ @api.view("/admin", guards=[IsAdminUser()])
241
+ class ViewWithClassGuards(APIView):
242
+ guards = [IsAuthenticated()]
243
+
244
+ async def get(self, request) -> dict:
245
+ return {"data": "test"}
246
+
247
+ # Verify route-level guards were used
248
+ handler_id = api._routes[0][2]
249
+ middleware_meta = api._handler_middleware.get(handler_id)
250
+ assert middleware_meta is not None
251
+ assert len(middleware_meta["guards"]) == 1
252
+ # Should be is_admin (IsAdminUser's guard_name), not is_authenticated
253
+ assert middleware_meta["guards"][0]["type"] == "is_admin"
254
+
255
+
256
+ def test_bolt_api_view_class_level_auth(api):
257
+ """Test class-level authentication backends."""
258
+
259
+ @api.view("/auth")
260
+ class AuthView(APIView):
261
+ auth = [JWTAuthentication()]
262
+
263
+ async def get(self, request) -> dict:
264
+ return {"authenticated": True}
265
+
266
+ # Verify middleware metadata includes auth backends
267
+ handler_id = api._routes[0][2]
268
+ middleware_meta = api._handler_middleware.get(handler_id)
269
+ assert middleware_meta is not None
270
+ assert "auth_backends" in middleware_meta
271
+
272
+
273
+ def test_bolt_api_view_status_code_override(api):
274
+ """Test class-level and route-level status code overrides."""
275
+
276
+ @api.view("/items")
277
+ class CreatedView(APIView):
278
+ status_code = 201
279
+
280
+ async def post(self, request) -> dict:
281
+ return {"created": True}
282
+
283
+ # Verify status code in handler metadata
284
+ handler = api._routes[0][3]
285
+ meta = api._handler_meta.get(handler)
286
+ assert meta is not None
287
+ assert meta.get("default_status_code") == 201
288
+
289
+
290
+ # --- Mixin Tests ---
291
+
292
+
293
+ @pytest.mark.asyncio
294
+ async def test_list_mixin(api):
295
+ """Test ListMixin provides get() method."""
296
+
297
+ # Mock queryset
298
+ class MockQuerySet:
299
+ def __init__(self, items):
300
+ self.items = items
301
+
302
+ def __aiter__(self):
303
+ return self
304
+
305
+ async def __anext__(self):
306
+ if not self.items:
307
+ raise StopAsyncIteration
308
+ return self.items.pop(0)
309
+
310
+ @api.view("/items")
311
+ class ItemListView(ListMixin, APIView):
312
+ async def get_queryset(self):
313
+ return MockQuerySet([{"id": 1}, {"id": 2}, {"id": 3}])
314
+
315
+ handler = api._routes[0][3]
316
+ request = create_request()
317
+
318
+ result = await handler(request)
319
+ assert isinstance(result, list)
320
+ assert len(result) == 3
321
+
322
+
323
+ @pytest.mark.asyncio
324
+ async def test_retrieve_mixin(api):
325
+ """Test RetrieveMixin provides get() with pk parameter."""
326
+
327
+ class MockObject:
328
+ def __init__(self, pk):
329
+ self.id = pk
330
+ self.name = f"Item {pk}"
331
+
332
+ @api.view("/items/{pk}")
333
+ class ItemRetrieveView(RetrieveMixin, APIView):
334
+ async def get_object(self, pk: int):
335
+ return MockObject(pk)
336
+
337
+ handler = api._routes[0][3]
338
+ request = create_request(path_params={"pk": "42"})
339
+
340
+ result = await handler(request, pk=42)
341
+ assert result.id == 42
342
+
343
+
344
+ @pytest.mark.asyncio
345
+ async def test_create_mixin(api):
346
+ """Test CreateMixin provides post() method."""
347
+
348
+ class ItemSchema(msgspec.Struct):
349
+ name: str
350
+ price: float
351
+
352
+ class MockModel:
353
+ objects = None
354
+
355
+ @staticmethod
356
+ async def acreate(**kwargs):
357
+ obj = type('MockObject', (), kwargs)()
358
+ obj.id = 1
359
+ return obj
360
+
361
+ class MockQuerySet:
362
+ model = MockModel
363
+
364
+ @api.view("/items")
365
+ class ItemCreateView(CreateMixin, APIView):
366
+ serializer_class = ItemSchema
367
+
368
+ async def get_queryset(self):
369
+ return MockQuerySet()
370
+
371
+ # Verify handler signature
372
+ handler = api._routes[0][3]
373
+ import inspect
374
+ sig = inspect.signature(handler)
375
+ assert "data" in sig.parameters
376
+
377
+
378
+ # --- ViewSet Tests ---
379
+
380
+
381
+ def test_bolt_viewset_get_allowed_methods():
382
+ """Test ViewSet correctly identifies implemented methods."""
383
+
384
+ class UserViewSet(ViewSet):
385
+ async def get(self, request):
386
+ return {"method": "list"}
387
+
388
+ async def post(self, request):
389
+ return {"method": "create"}
390
+
391
+ allowed = UserViewSet.get_allowed_methods()
392
+ assert "GET" in allowed
393
+ assert "POST" in allowed
394
+ assert "DELETE" not in allowed
395
+
396
+
397
+ @pytest.mark.asyncio
398
+ async def test_bolt_viewset_get_object_not_found():
399
+ """Test ViewSet.get_object raises HTTPException when object not found."""
400
+
401
+ class MockQuerySet:
402
+ async def aget(self, pk):
403
+ raise Exception("DoesNotExist")
404
+
405
+ class ItemViewSet(ViewSet):
406
+ async def get_queryset(self):
407
+ return MockQuerySet()
408
+
409
+ viewset = ItemViewSet()
410
+
411
+ with pytest.raises(HTTPException) as exc_info:
412
+ await viewset.get_object(999)
413
+
414
+ assert exc_info.value.status_code == 404
415
+
416
+
417
+ # --- Edge Cases and Validation ---
418
+
419
+
420
+ def test_bolt_api_view_non_async_handler_raises():
421
+ """Test that non-async handlers raise TypeError."""
422
+
423
+ api = BoltAPI()
424
+
425
+ with pytest.raises(TypeError) as exc_info:
426
+ @api.view("/bad")
427
+ class BadView(APIView):
428
+ def get(self, request): # NOT async
429
+ return {"bad": True}
430
+
431
+ assert "must be async" in str(exc_info.value)
432
+
433
+
434
+ def test_bolt_api_view_non_subclass_raises():
435
+ """Test that non-APIView classes raise TypeError."""
436
+
437
+ api = BoltAPI()
438
+
439
+ with pytest.raises(TypeError) as exc_info:
440
+ @api.view("/bad")
441
+ class NotAView:
442
+ async def get(self, request):
443
+ return {}
444
+
445
+ assert "must inherit from APIView" in str(exc_info.value)
446
+
447
+
448
+ def test_bolt_api_view_no_methods_raises():
449
+ """Test that view with no methods raises ValueError."""
450
+
451
+ api = BoltAPI()
452
+
453
+ with pytest.raises(ValueError) as exc_info:
454
+ @api.view("/empty")
455
+ class EmptyView(APIView):
456
+ http_method_names = []
457
+
458
+ assert "does not implement any HTTP methods" in str(exc_info.value)
459
+
460
+
461
+ def test_bolt_api_view_selective_method_registration(api):
462
+ """Test registering only specific methods from a view."""
463
+
464
+ # Only register GET and POST
465
+ @api.view("/items", methods=["GET", "POST"])
466
+ class MultiMethodView(APIView):
467
+ async def get(self, request) -> dict:
468
+ return {"method": "GET"}
469
+
470
+ async def post(self, request) -> dict:
471
+ return {"method": "POST"}
472
+
473
+ async def delete(self, request) -> dict:
474
+ return {"method": "DELETE"}
475
+
476
+ # Verify only 2 methods registered
477
+ assert len(api._routes) == 2
478
+ methods = {route[0] for route in api._routes}
479
+ assert methods == {"GET", "POST"}
480
+ assert "DELETE" not in methods
481
+
482
+
483
+ def test_bolt_api_view_unimplemented_method_raises(api):
484
+ """Test requesting unimplemented method raises ValueError."""
485
+
486
+ with pytest.raises(ValueError) as exc_info:
487
+ @api.view("/items", methods=["POST"])
488
+ class GetOnlyView(APIView):
489
+ async def get(self, request) -> dict:
490
+ return {"method": "GET"}
491
+
492
+ assert "does not implement method 'post'" in str(exc_info.value)
493
+
494
+
495
+ # --- Integration Tests ---
496
+
497
+
498
+ @pytest.mark.asyncio
499
+ async def test_complete_crud_viewset(api):
500
+ """Test a complete CRUD viewset with all mixins."""
501
+
502
+ class ItemSchema(msgspec.Struct):
503
+ id: int
504
+ name: str
505
+
506
+ # Mock database
507
+ mock_db = {1: {"id": 1, "name": "Item 1"}, 2: {"id": 2, "name": "Item 2"}}
508
+
509
+ class MockQuerySet:
510
+ def __init__(self, items):
511
+ self.items = items
512
+ self.model = type('MockModel', (), {
513
+ 'objects': type('MockManager', (), {
514
+ 'acreate': lambda **kw: type('MockObj', (), {**kw, 'asave': lambda: None, 'adelete': lambda: None})()
515
+ })()
516
+ })
517
+
518
+ def __aiter__(self):
519
+ self.iterator = iter(self.items.values())
520
+ return self
521
+
522
+ async def __anext__(self):
523
+ try:
524
+ return next(self.iterator)
525
+ except StopIteration:
526
+ raise StopAsyncIteration
527
+
528
+ async def aget(self, pk):
529
+ if pk not in self.items:
530
+ raise Exception("DoesNotExist")
531
+ item = self.items[pk]
532
+ return type('MockObj', (), {**item, 'asave': lambda: None, 'adelete': lambda: None})()
533
+
534
+ @api.view("/items", methods=["GET"])
535
+ @api.view("/items/{pk}", methods=["GET", "DELETE"])
536
+ class ItemViewSet(
537
+ ListMixin,
538
+ RetrieveMixin,
539
+ CreateMixin,
540
+ UpdateMixin,
541
+ DestroyMixin,
542
+ ViewSet
543
+ ):
544
+ serializer_class = ItemSchema
545
+
546
+ async def get_queryset(self):
547
+ return MockQuerySet(mock_db)
548
+
549
+ # Verify routes registered
550
+ assert len(api._routes) == 3 # list GET, retrieve GET, destroy DELETE
551
+
552
+
553
+ def test_bolt_api_view_method_names_customization():
554
+ """Test customizing http_method_names."""
555
+
556
+ api = BoltAPI()
557
+
558
+ @api.view("/limited")
559
+ class GetOnlyView(APIView):
560
+ http_method_names = ["get"]
561
+
562
+ async def get(self, request) -> dict:
563
+ return {"method": "GET"}
564
+
565
+ async def post(self, request) -> dict:
566
+ return {"method": "POST"}
567
+
568
+ # Only GET should be registered (POST not in http_method_names)
569
+ assert len(api._routes) == 1
570
+ assert api._routes[0][0] == "GET"