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,400 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Integration tests for parameter validation with real-world use cases.
|
|
3
|
-
|
|
4
|
-
Tests the complete flow from route registration to request handling.
|
|
5
|
-
"""
|
|
6
|
-
import pytest
|
|
7
|
-
import msgspec
|
|
8
|
-
from django_bolt import BoltAPI
|
|
9
|
-
from django_bolt.params import Query, Body, Header, Cookie, Form, Path
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class User(msgspec.Struct):
|
|
13
|
-
id: int
|
|
14
|
-
name: str
|
|
15
|
-
email: str
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class UserCreate(msgspec.Struct):
|
|
19
|
-
name: str
|
|
20
|
-
email: str
|
|
21
|
-
password: str
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class UserUpdate(msgspec.Struct):
|
|
25
|
-
name: str | None = None
|
|
26
|
-
email: str | None = None
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class SearchFilters(msgspec.Struct):
|
|
30
|
-
query: str
|
|
31
|
-
tags: list[str] = []
|
|
32
|
-
min_score: float = 0.0
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
# ============================================================================
|
|
36
|
-
# Use Case 1: CRUD Operations with Proper HTTP Methods
|
|
37
|
-
# ============================================================================
|
|
38
|
-
|
|
39
|
-
def test_crud_operations_validation():
|
|
40
|
-
"""Test that CRUD operations use correct HTTP methods and parameters."""
|
|
41
|
-
api = BoltAPI()
|
|
42
|
-
|
|
43
|
-
# ✓ List (GET with query params)
|
|
44
|
-
@api.get("/users")
|
|
45
|
-
async def list_users(page: int = 1, limit: int = 10, search: str = ""):
|
|
46
|
-
return {"users": [], "page": page, "limit": limit}
|
|
47
|
-
|
|
48
|
-
# ✓ Get single (GET with path param)
|
|
49
|
-
@api.get("/users/{user_id}")
|
|
50
|
-
async def get_user(user_id: int):
|
|
51
|
-
return {"id": user_id, "name": "John"}
|
|
52
|
-
|
|
53
|
-
# ✓ Create (POST with body)
|
|
54
|
-
@api.post("/users")
|
|
55
|
-
async def create_user(user: UserCreate):
|
|
56
|
-
return {"id": 1, "name": user.name}
|
|
57
|
-
|
|
58
|
-
# ✓ Update (PUT with path + body)
|
|
59
|
-
@api.put("/users/{user_id}")
|
|
60
|
-
async def update_user(user_id: int, user: UserUpdate):
|
|
61
|
-
return {"id": user_id, "updated": True}
|
|
62
|
-
|
|
63
|
-
# ✓ Partial update (PATCH with path + body)
|
|
64
|
-
@api.patch("/users/{user_id}")
|
|
65
|
-
async def patch_user(user_id: int, user: UserUpdate):
|
|
66
|
-
return {"id": user_id, "patched": True}
|
|
67
|
-
|
|
68
|
-
# ✓ Delete (DELETE with path param, no body)
|
|
69
|
-
@api.delete("/users/{user_id}")
|
|
70
|
-
async def delete_user(user_id: int):
|
|
71
|
-
return {"deleted": True}
|
|
72
|
-
|
|
73
|
-
assert len(api._routes) == 6
|
|
74
|
-
|
|
75
|
-
# Verify parameter sources
|
|
76
|
-
for method, path, handler_id, fn in api._routes:
|
|
77
|
-
meta = api._handler_meta[fn]
|
|
78
|
-
|
|
79
|
-
if method == "GET" and path == "/users":
|
|
80
|
-
# Query params should be inferred
|
|
81
|
-
for field in meta["fields"]:
|
|
82
|
-
assert field["source"] == "query"
|
|
83
|
-
|
|
84
|
-
elif method == "GET" and "user_id" in path:
|
|
85
|
-
# Path param should be detected
|
|
86
|
-
user_id_field = next(f for f in meta["fields"] if f["name"] == "user_id")
|
|
87
|
-
assert user_id_field["source"] == "path"
|
|
88
|
-
|
|
89
|
-
elif method in ("POST", "PUT", "PATCH"):
|
|
90
|
-
# Should have body params
|
|
91
|
-
body_fields = [f for f in meta["fields"] if f["source"] == "body"]
|
|
92
|
-
if "POST" in method or "PUT" in method or "PATCH" in method:
|
|
93
|
-
# At least one body field expected
|
|
94
|
-
assert len(body_fields) >= 1 or any(
|
|
95
|
-
f["source"] == "path" for f in meta["fields"]
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
# ============================================================================
|
|
100
|
-
# Use Case 2: Search/Filter Endpoints
|
|
101
|
-
# ============================================================================
|
|
102
|
-
|
|
103
|
-
def test_search_endpoints_with_complex_filters():
|
|
104
|
-
"""Test search endpoints with multiple query parameters."""
|
|
105
|
-
api = BoltAPI()
|
|
106
|
-
|
|
107
|
-
# ✓ Simple search with auto-inference
|
|
108
|
-
@api.get("/search")
|
|
109
|
-
async def simple_search(q: str, limit: int = 10):
|
|
110
|
-
return {"results": [], "query": q}
|
|
111
|
-
|
|
112
|
-
# ✓ Advanced search with explicit markers
|
|
113
|
-
@api.get("/advanced-search")
|
|
114
|
-
async def advanced_search(
|
|
115
|
-
query: str = Query(min_length=3),
|
|
116
|
-
category: str = Query(default="all"),
|
|
117
|
-
min_price: float = Query(ge=0, default=0),
|
|
118
|
-
max_price: float = Query(le=10000, default=10000),
|
|
119
|
-
page: int = Query(ge=1, default=1)
|
|
120
|
-
):
|
|
121
|
-
return {"results": []}
|
|
122
|
-
|
|
123
|
-
# ✓ Search with struct body (POST for complex filters)
|
|
124
|
-
@api.post("/search/advanced")
|
|
125
|
-
async def complex_search(filters: SearchFilters):
|
|
126
|
-
return {"results": [], "filters": filters}
|
|
127
|
-
|
|
128
|
-
assert len(api._routes) == 3
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
# ============================================================================
|
|
132
|
-
# Use Case 3: Authentication/Authorization Headers
|
|
133
|
-
# ============================================================================
|
|
134
|
-
|
|
135
|
-
def test_authentication_headers():
|
|
136
|
-
"""Test endpoints with authentication headers."""
|
|
137
|
-
api = BoltAPI()
|
|
138
|
-
|
|
139
|
-
@api.get("/protected")
|
|
140
|
-
async def protected_endpoint(
|
|
141
|
-
authorization: str = Header(alias="Authorization"),
|
|
142
|
-
user_agent: str = Header(alias="User-Agent", default="unknown")
|
|
143
|
-
):
|
|
144
|
-
return {"authenticated": True}
|
|
145
|
-
|
|
146
|
-
@api.get("/api-key")
|
|
147
|
-
async def api_key_endpoint(
|
|
148
|
-
api_key: str = Header(alias="X-API-Key")
|
|
149
|
-
):
|
|
150
|
-
return {"valid": True}
|
|
151
|
-
|
|
152
|
-
assert len(api._routes) == 2
|
|
153
|
-
|
|
154
|
-
# Verify header sources
|
|
155
|
-
for method, path, handler_id, fn in api._routes:
|
|
156
|
-
meta = api._handler_meta[fn]
|
|
157
|
-
for field in meta["fields"]:
|
|
158
|
-
assert field["source"] == "header"
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
# ============================================================================
|
|
162
|
-
# Use Case 4: File Upload Endpoints
|
|
163
|
-
# ============================================================================
|
|
164
|
-
|
|
165
|
-
def test_file_upload_endpoints():
|
|
166
|
-
"""Test file upload with form data."""
|
|
167
|
-
from django_bolt.params import File
|
|
168
|
-
|
|
169
|
-
api = BoltAPI()
|
|
170
|
-
|
|
171
|
-
@api.post("/upload")
|
|
172
|
-
async def upload_file(
|
|
173
|
-
file: bytes = File(),
|
|
174
|
-
description: str = Form(default="")
|
|
175
|
-
):
|
|
176
|
-
return {"uploaded": True, "size": len(file)}
|
|
177
|
-
|
|
178
|
-
@api.post("/upload/multiple")
|
|
179
|
-
async def upload_multiple(
|
|
180
|
-
files: list[bytes] = File(),
|
|
181
|
-
category: str = Form()
|
|
182
|
-
):
|
|
183
|
-
return {"uploaded": len(files)}
|
|
184
|
-
|
|
185
|
-
assert len(api._routes) == 2
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
# ============================================================================
|
|
189
|
-
# Use Case 5: Mixed Parameter Sources
|
|
190
|
-
# ============================================================================
|
|
191
|
-
|
|
192
|
-
def test_mixed_parameter_sources():
|
|
193
|
-
"""Test endpoints with parameters from multiple sources."""
|
|
194
|
-
api = BoltAPI()
|
|
195
|
-
|
|
196
|
-
@api.get("/items/{item_id}")
|
|
197
|
-
async def get_item_with_options(
|
|
198
|
-
item_id: int, # Path
|
|
199
|
-
include_details: bool = False, # Query (inferred)
|
|
200
|
-
user_agent: str = Header(alias="User-Agent", default="unknown"), # Header
|
|
201
|
-
session_id: str = Cookie(default="") # Cookie
|
|
202
|
-
):
|
|
203
|
-
return {"item_id": item_id, "details": include_details}
|
|
204
|
-
|
|
205
|
-
meta = api._handler_meta[api._routes[0][3]]
|
|
206
|
-
|
|
207
|
-
sources = {f["name"]: f["source"] for f in meta["fields"]}
|
|
208
|
-
assert sources["item_id"] == "path"
|
|
209
|
-
assert sources["include_details"] == "query"
|
|
210
|
-
assert sources["user_agent"] == "header"
|
|
211
|
-
assert sources["session_id"] == "cookie"
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
# ============================================================================
|
|
215
|
-
# Use Case 6: Validation Error Cases
|
|
216
|
-
# ============================================================================
|
|
217
|
-
|
|
218
|
-
def test_invalid_get_with_body():
|
|
219
|
-
"""Test that GET with body parameter is rejected."""
|
|
220
|
-
api = BoltAPI()
|
|
221
|
-
|
|
222
|
-
with pytest.raises(TypeError) as exc_info:
|
|
223
|
-
@api.get("/invalid")
|
|
224
|
-
async def invalid_get(data: UserCreate):
|
|
225
|
-
return {"error": "should not register"}
|
|
226
|
-
|
|
227
|
-
assert "GET /invalid cannot have body parameters" in str(exc_info.value)
|
|
228
|
-
assert "data" in str(exc_info.value)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def test_invalid_delete_with_body():
|
|
232
|
-
"""Test that DELETE with body parameter is rejected."""
|
|
233
|
-
api = BoltAPI()
|
|
234
|
-
|
|
235
|
-
with pytest.raises(TypeError) as exc_info:
|
|
236
|
-
@api.delete("/items/{item_id}")
|
|
237
|
-
async def invalid_delete(item_id: int, data: UserCreate):
|
|
238
|
-
return {"deleted": True}
|
|
239
|
-
|
|
240
|
-
assert "DELETE" in str(exc_info.value)
|
|
241
|
-
assert "cannot have body parameters" in str(exc_info.value)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
# ============================================================================
|
|
245
|
-
# Use Case 7: Nested Routes with Path Parameters
|
|
246
|
-
# ============================================================================
|
|
247
|
-
|
|
248
|
-
def test_nested_resources_with_path_params():
|
|
249
|
-
"""Test nested resource routes with multiple path parameters."""
|
|
250
|
-
api = BoltAPI()
|
|
251
|
-
|
|
252
|
-
@api.get("/users/{user_id}/posts/{post_id}")
|
|
253
|
-
async def get_user_post(user_id: int, post_id: int):
|
|
254
|
-
return {"user_id": user_id, "post_id": post_id}
|
|
255
|
-
|
|
256
|
-
@api.get("/organizations/{org_id}/teams/{team_id}/members/{member_id}")
|
|
257
|
-
async def get_team_member(org_id: int, team_id: int, member_id: int):
|
|
258
|
-
return {"org": org_id, "team": team_id, "member": member_id}
|
|
259
|
-
|
|
260
|
-
# Verify all path params detected
|
|
261
|
-
for method, path, handler_id, fn in api._routes:
|
|
262
|
-
meta = api._handler_meta[fn]
|
|
263
|
-
path_fields = [f for f in meta["fields"] if f["source"] == "path"]
|
|
264
|
-
|
|
265
|
-
# Count expected path params
|
|
266
|
-
import re
|
|
267
|
-
expected_params = len(re.findall(r'\{(\w+)\}', path))
|
|
268
|
-
assert len(path_fields) == expected_params
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
# ============================================================================
|
|
272
|
-
# Use Case 8: Optional vs Required Parameters
|
|
273
|
-
# ============================================================================
|
|
274
|
-
|
|
275
|
-
def test_optional_and_required_parameters():
|
|
276
|
-
"""Test proper handling of optional and required parameters."""
|
|
277
|
-
api = BoltAPI()
|
|
278
|
-
|
|
279
|
-
@api.get("/items")
|
|
280
|
-
async def list_items(
|
|
281
|
-
category: str, # Required (no default)
|
|
282
|
-
min_price: float = 0, # Optional (has default)
|
|
283
|
-
max_price: float | None = None, # Optional (nullable)
|
|
284
|
-
tags: list[str] = [] # Optional (default empty list)
|
|
285
|
-
):
|
|
286
|
-
return {"items": []}
|
|
287
|
-
|
|
288
|
-
meta = api._handler_meta[api._routes[0][3]]
|
|
289
|
-
|
|
290
|
-
category_field = next(f for f in meta["fields"] if f["name"] == "category")
|
|
291
|
-
min_price_field = next(f for f in meta["fields"] if f["name"] == "min_price")
|
|
292
|
-
max_price_field = next(f for f in meta["fields"] if f["name"] == "max_price")
|
|
293
|
-
|
|
294
|
-
# Check field definitions
|
|
295
|
-
assert category_field["field_def"].is_required
|
|
296
|
-
assert not min_price_field["field_def"].is_required
|
|
297
|
-
assert not max_price_field["field_def"].is_required
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
# ============================================================================
|
|
301
|
-
# Use Case 9: Explicit Path() Marker (Edge Case)
|
|
302
|
-
# ============================================================================
|
|
303
|
-
|
|
304
|
-
def test_explicit_path_marker():
|
|
305
|
-
"""Test explicit Path() marker (rarely needed but supported)."""
|
|
306
|
-
api = BoltAPI()
|
|
307
|
-
|
|
308
|
-
@api.get("/items/{item_id}")
|
|
309
|
-
async def get_item(item_id: int = Path()):
|
|
310
|
-
return {"id": item_id}
|
|
311
|
-
|
|
312
|
-
meta = api._handler_meta[api._routes[0][3]]
|
|
313
|
-
item_id_field = next(f for f in meta["fields"] if f["name"] == "item_id")
|
|
314
|
-
assert item_id_field["source"] == "path"
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
# ============================================================================
|
|
318
|
-
# Use Case 10: Complex Real-World API
|
|
319
|
-
# ============================================================================
|
|
320
|
-
|
|
321
|
-
def test_real_world_api_structure():
|
|
322
|
-
"""Test a realistic API with multiple endpoint types."""
|
|
323
|
-
api = BoltAPI(prefix="/api/v1")
|
|
324
|
-
|
|
325
|
-
# Auth endpoints
|
|
326
|
-
@api.post("/auth/login")
|
|
327
|
-
async def login(email: str = Form(), password: str = Form()):
|
|
328
|
-
return {"token": "abc123"}
|
|
329
|
-
|
|
330
|
-
@api.post("/auth/logout")
|
|
331
|
-
async def logout(authorization: str = Header(alias="Authorization")):
|
|
332
|
-
return {"logged_out": True}
|
|
333
|
-
|
|
334
|
-
# User management
|
|
335
|
-
@api.get("/users/{user_id}")
|
|
336
|
-
async def get_user(user_id: int, include_posts: bool = False):
|
|
337
|
-
return {"id": user_id}
|
|
338
|
-
|
|
339
|
-
@api.put("/users/{user_id}")
|
|
340
|
-
async def update_user(user_id: int, user: UserUpdate):
|
|
341
|
-
return {"updated": True}
|
|
342
|
-
|
|
343
|
-
# Search
|
|
344
|
-
@api.get("/search")
|
|
345
|
-
async def search(q: str, type: str = "all", page: int = 1):
|
|
346
|
-
return {"results": []}
|
|
347
|
-
|
|
348
|
-
# Analytics (complex filters via POST)
|
|
349
|
-
@api.post("/analytics/report")
|
|
350
|
-
async def generate_report(filters: SearchFilters):
|
|
351
|
-
return {"report_id": "r123"}
|
|
352
|
-
|
|
353
|
-
assert len(api._routes) == 6
|
|
354
|
-
|
|
355
|
-
# Verify no GET endpoints have body params
|
|
356
|
-
for method, path, handler_id, fn in api._routes:
|
|
357
|
-
if method == "GET":
|
|
358
|
-
meta = api._handler_meta[fn]
|
|
359
|
-
body_fields = [f for f in meta["fields"] if f["source"] == "body"]
|
|
360
|
-
assert len(body_fields) == 0, f"GET {path} should not have body params"
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
# ============================================================================
|
|
364
|
-
# Use Case 11: Error Messages Quality
|
|
365
|
-
# ============================================================================
|
|
366
|
-
|
|
367
|
-
def test_error_message_includes_all_solutions():
|
|
368
|
-
"""Verify error messages provide all necessary information."""
|
|
369
|
-
api = BoltAPI()
|
|
370
|
-
|
|
371
|
-
with pytest.raises(TypeError) as exc_info:
|
|
372
|
-
@api.get("/bad-endpoint")
|
|
373
|
-
async def bad_handler(complex_data: User):
|
|
374
|
-
return {"data": complex_data}
|
|
375
|
-
|
|
376
|
-
error_msg = str(exc_info.value)
|
|
377
|
-
|
|
378
|
-
# Should include handler name
|
|
379
|
-
assert "bad_handler" in error_msg
|
|
380
|
-
|
|
381
|
-
# Should include method and path
|
|
382
|
-
assert "GET" in error_msg
|
|
383
|
-
assert "/bad-endpoint" in error_msg
|
|
384
|
-
|
|
385
|
-
# Should list problematic parameters
|
|
386
|
-
assert "complex_data" in error_msg
|
|
387
|
-
|
|
388
|
-
# Should provide solutions
|
|
389
|
-
assert "Solutions:" in error_msg
|
|
390
|
-
assert "POST/PUT/PATCH" in error_msg
|
|
391
|
-
assert "Query()" in error_msg
|
|
392
|
-
assert "simple types" in error_msg
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
# ============================================================================
|
|
396
|
-
# Run all tests
|
|
397
|
-
# ============================================================================
|
|
398
|
-
|
|
399
|
-
if __name__ == "__main__":
|
|
400
|
-
pytest.main([__file__, "-v"])
|