django-bolt 0.1.0__cp310-abi3-win_amd64.whl → 0.1.2__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.2.dist-info}/METADATA +181 -201
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.2.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.2.dist-info}/WHEEL +0 -0
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.2.dist-info}/entry_points.txt +0 -0
|
@@ -1,570 +0,0 @@
|
|
|
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"
|