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.

Files changed (56) hide show
  1. django_bolt/__init__.py +2 -2
  2. django_bolt/_core.pyd +0 -0
  3. django_bolt/_json.py +169 -0
  4. django_bolt/admin/static_routes.py +15 -21
  5. django_bolt/api.py +181 -61
  6. django_bolt/auth/__init__.py +2 -2
  7. django_bolt/decorators.py +15 -3
  8. django_bolt/dependencies.py +30 -24
  9. django_bolt/error_handlers.py +2 -1
  10. django_bolt/openapi/plugins.py +3 -2
  11. django_bolt/openapi/schema_generator.py +65 -20
  12. django_bolt/pagination.py +2 -1
  13. django_bolt/responses.py +3 -2
  14. django_bolt/serialization.py +5 -4
  15. {django_bolt-0.1.0.dist-info → django_bolt-0.1.1.dist-info}/METADATA +179 -197
  16. {django_bolt-0.1.0.dist-info → django_bolt-0.1.1.dist-info}/RECORD +18 -55
  17. django_bolt/auth/README.md +0 -464
  18. django_bolt/auth/REVOCATION_EXAMPLE.md +0 -391
  19. django_bolt/tests/__init__.py +0 -0
  20. django_bolt/tests/admin_tests/__init__.py +0 -1
  21. django_bolt/tests/admin_tests/conftest.py +0 -6
  22. django_bolt/tests/admin_tests/test_admin_with_django.py +0 -278
  23. django_bolt/tests/admin_tests/urls.py +0 -9
  24. django_bolt/tests/cbv/__init__.py +0 -0
  25. django_bolt/tests/cbv/test_class_views.py +0 -570
  26. django_bolt/tests/cbv/test_class_views_django_orm.py +0 -703
  27. django_bolt/tests/cbv/test_class_views_features.py +0 -1173
  28. django_bolt/tests/cbv/test_class_views_with_client.py +0 -622
  29. django_bolt/tests/conftest.py +0 -165
  30. django_bolt/tests/test_action_decorator.py +0 -399
  31. django_bolt/tests/test_auth_secret_key.py +0 -83
  32. django_bolt/tests/test_decorator_syntax.py +0 -159
  33. django_bolt/tests/test_error_handling.py +0 -481
  34. django_bolt/tests/test_file_response.py +0 -192
  35. django_bolt/tests/test_global_cors.py +0 -172
  36. django_bolt/tests/test_guards_auth.py +0 -441
  37. django_bolt/tests/test_guards_integration.py +0 -303
  38. django_bolt/tests/test_health.py +0 -283
  39. django_bolt/tests/test_integration_validation.py +0 -400
  40. django_bolt/tests/test_json_validation.py +0 -536
  41. django_bolt/tests/test_jwt_auth.py +0 -327
  42. django_bolt/tests/test_jwt_token.py +0 -458
  43. django_bolt/tests/test_logging.py +0 -837
  44. django_bolt/tests/test_logging_merge.py +0 -419
  45. django_bolt/tests/test_middleware.py +0 -492
  46. django_bolt/tests/test_middleware_server.py +0 -230
  47. django_bolt/tests/test_model_viewset.py +0 -323
  48. django_bolt/tests/test_models.py +0 -24
  49. django_bolt/tests/test_pagination.py +0 -1258
  50. django_bolt/tests/test_parameter_validation.py +0 -178
  51. django_bolt/tests/test_syntax.py +0 -626
  52. django_bolt/tests/test_testing_utilities.py +0 -163
  53. django_bolt/tests/test_testing_utilities_simple.py +0 -123
  54. django_bolt/tests/test_viewset_unified.py +0 -346
  55. {django_bolt-0.1.0.dist-info → django_bolt-0.1.1.dist-info}/WHEEL +0 -0
  56. {django_bolt-0.1.0.dist-info → django_bolt-0.1.1.dist-info}/entry_points.txt +0 -0
@@ -1,391 +0,0 @@
1
- # Token Revocation Examples
2
-
3
- Token revocation is **OPTIONAL** in Django-Bolt. Only use it if you need logout functionality or token invalidation.
4
-
5
- ## Quick Start
6
-
7
- ### Option 1: In-Memory (Development/Single-Process)
8
-
9
- ```python
10
- from django_bolt import BoltAPI, JWTAuthentication, Token, IsAuthenticated
11
- from django_bolt.auth.revocation import InMemoryRevocation
12
- from datetime import timedelta
13
- import uuid
14
-
15
- # Create revocation store
16
- revocation = InMemoryRevocation()
17
-
18
- # Create API with revocation-enabled auth
19
- api = BoltAPI()
20
-
21
- auth = JWTAuthentication(
22
- secret=settings.SECRET_KEY,
23
- revocation_store=revocation, # ← OPTIONAL: Enables revocation
24
- require_jti=True, # ← Auto-enabled when revocation_store is provided
25
- )
26
-
27
- # Login endpoint - create tokens with JTI
28
- @api.post("/login")
29
- async def login(username: str, password: str):
30
- user = await authenticate(username=username, password=password)
31
-
32
- if not user:
33
- return {"error": "Invalid credentials"}, 401
34
-
35
- # Create token with JTI (required for revocation)
36
- token = Token.create(
37
- sub=str(user.id),
38
- expires_delta=timedelta(hours=1),
39
- jti=str(uuid.uuid4()), # ← Unique token ID (required for revocation)
40
- is_staff=user.is_staff,
41
- is_admin=user.is_superuser,
42
- )
43
-
44
- return {
45
- "access_token": token.encode(settings.SECRET_KEY),
46
- "token_type": "bearer"
47
- }
48
-
49
- # Logout endpoint - revoke token
50
- @api.post("/logout", auth=[auth], guards=[IsAuthenticated()])
51
- async def logout(request):
52
- jti = request["context"]["auth_claims"]["jti"]
53
-
54
- # Revoke the token
55
- await revocation.revoke(jti, ttl=3600) # TTL = remaining token lifetime
56
-
57
- return {"message": "Logged out successfully"}
58
-
59
- # Protected endpoint
60
- @api.get("/profile", auth=[auth], guards=[IsAuthenticated()])
61
- async def get_profile(request):
62
- user_id = request["context"]["user_id"]
63
- return {"user_id": user_id}
64
- ```
65
-
66
- ---
67
-
68
- ### Option 2: Django Cache (Production - Redis/Memcached)
69
-
70
- ```python
71
- from django_bolt import BoltAPI, JWTAuthentication
72
- from django_bolt.auth.revocation import DjangoCacheRevocation
73
-
74
- # settings.py
75
- CACHES = {
76
- 'default': {
77
- 'BACKEND': 'django.core.cache.backends.redis.RedisCache',
78
- 'LOCATION': 'redis://127.0.0.1:6379/1',
79
- }
80
- }
81
-
82
- # api.py
83
- revocation = DjangoCacheRevocation(
84
- cache_alias='default',
85
- key_prefix='revoked:', # Keys will be "revoked:{jti}"
86
- )
87
-
88
- auth = JWTAuthentication(
89
- secret=settings.SECRET_KEY,
90
- revocation_store=revocation,
91
- )
92
-
93
- @api.post("/logout", auth=[auth])
94
- async def logout(request):
95
- jti = request["context"]["auth_claims"]["jti"]
96
-
97
- # Revoke token - stored in Redis with TTL
98
- await revocation.revoke(jti, ttl=86400 * 30) # 30 days
99
-
100
- return {"message": "Token revoked"}
101
- ```
102
-
103
- ---
104
-
105
- ### Option 3: Database (No Cache Infrastructure)
106
-
107
- ```python
108
- # myapp/models.py
109
- from django.db import models
110
-
111
- class RevokedToken(models.Model):
112
- jti = models.CharField(max_length=255, unique=True, db_index=True)
113
- revoked_at = models.DateTimeField(auto_now_add=True)
114
- expires_at = models.DateTimeField(db_index=True)
115
-
116
- class Meta:
117
- indexes = [
118
- models.Index(fields=['jti']),
119
- models.Index(fields=['expires_at']),
120
- ]
121
- ```
122
-
123
- ```python
124
- # api.py
125
- from django_bolt.auth.revocation import DjangoORMRevocation
126
-
127
- revocation = DjangoORMRevocation(model='myapp.RevokedToken')
128
-
129
- auth = JWTAuthentication(
130
- secret=settings.SECRET_KEY,
131
- revocation_store=revocation,
132
- )
133
-
134
- @api.post("/logout", auth=[auth])
135
- async def logout(request):
136
- jti = request["context"]["auth_claims"]["jti"]
137
-
138
- # Revoke token - stored in database
139
- await revocation.revoke(jti, ttl=86400 * 30)
140
-
141
- return {"message": "Token revoked"}
142
- ```
143
-
144
- **⚠️ Important**: Add cleanup task for expired tokens:
145
-
146
- ```python
147
- # Celery task or cron job
148
- from datetime import datetime, timezone
149
- from myapp.models import RevokedToken
150
-
151
- async def cleanup_expired_tokens():
152
- """Run this periodically (e.g., daily)"""
153
- await RevokedToken.objects.filter(
154
- expires_at__lt=datetime.now(timezone.utc)
155
- ).adelete()
156
- ```
157
-
158
- ---
159
-
160
- ### Option 4: Custom Revocation Handler
161
-
162
- If you have custom logic or use a different storage backend:
163
-
164
- ```python
165
- from django_bolt import JWTAuthentication
166
- import httpx
167
-
168
- # Custom handler - checks external service
169
- async def check_token_revoked(jti: str) -> bool:
170
- """Custom revocation logic"""
171
- async with httpx.AsyncClient() as client:
172
- response = await client.get(f"https://auth-service/check-revoked/{jti}")
173
- return response.json()["revoked"]
174
-
175
- auth = JWTAuthentication(
176
- secret=settings.SECRET_KEY,
177
- revoked_token_handler=check_token_revoked, # ← Custom function
178
- require_jti=True,
179
- )
180
- ```
181
-
182
- ---
183
-
184
- ## Without Revocation (Simpler, Faster)
185
-
186
- If you don't need logout functionality, just don't provide a revocation handler:
187
-
188
- ```python
189
- # NO revocation - tokens valid until expiry
190
- auth = JWTAuthentication(
191
- secret=settings.SECRET_KEY,
192
- # No revocation_store or revoked_token_handler
193
- )
194
-
195
- # Users can't logout - tokens expire naturally
196
- # ✅ Simpler
197
- # ✅ Faster (~60k RPS vs ~50k RPS with revocation)
198
- # ❌ No logout support
199
- ```
200
-
201
- ---
202
-
203
- ## Performance Comparison
204
-
205
- | Revocation Strategy | RPS | Multi-Process | Notes |
206
- |---------------------|-----|---------------|-------|
207
- | **No Revocation** | ~60k | ✅ | Fastest, no logout |
208
- | **InMemoryRevocation** | ~58k | ❌ | Single process only |
209
- | **DjangoCacheRevocation (Redis)** | ~50k | ✅ | **Recommended for production** |
210
- | **DjangoCacheRevocation (Memcached)** | ~48k | ✅ | Good alternative |
211
- | **DjangoORMRevocation (PostgreSQL)** | ~5k | ✅ | Slowest, use if no cache |
212
- | **DjangoORMRevocation (SQLite)** | ~2k | ⚠️ | Not for production |
213
-
214
- ---
215
-
216
- ## Complete Login/Logout Example
217
-
218
- ```python
219
- from django_bolt import BoltAPI, JWTAuthentication, Token, IsAuthenticated, AllowAny
220
- from django_bolt.auth.revocation import DjangoCacheRevocation
221
- from django.contrib.auth import authenticate
222
- from datetime import timedelta
223
- import uuid
224
-
225
- api = BoltAPI()
226
-
227
- # Setup revocation
228
- revocation = DjangoCacheRevocation()
229
-
230
- auth = JWTAuthentication(
231
- secret=settings.SECRET_KEY,
232
- revocation_store=revocation,
233
- )
234
-
235
- # Public login endpoint
236
- @api.post("/auth/login", guards=[AllowAny()])
237
- async def login(username: str, password: str):
238
- """Authenticate and return JWT token."""
239
- user = await authenticate(username=username, password=password)
240
-
241
- if not user:
242
- return {"error": "Invalid credentials"}, 401
243
-
244
- # Create access token (short-lived)
245
- access_token = Token.create(
246
- sub=str(user.id),
247
- expires_delta=timedelta(minutes=15),
248
- jti=str(uuid.uuid4()), # Required for revocation
249
- is_staff=user.is_staff,
250
- is_admin=user.is_superuser,
251
- permissions=list(user.get_all_permissions()),
252
- extras={
253
- "username": user.username,
254
- "email": user.email,
255
- }
256
- )
257
-
258
- # Optional: Create refresh token (long-lived)
259
- refresh_token = Token.create(
260
- sub=str(user.id),
261
- expires_delta=timedelta(days=30),
262
- jti=str(uuid.uuid4()),
263
- extras={"type": "refresh"}
264
- )
265
-
266
- return {
267
- "access_token": access_token.encode(settings.SECRET_KEY),
268
- "refresh_token": refresh_token.encode(settings.SECRET_KEY),
269
- "token_type": "bearer",
270
- "expires_in": 900 # 15 minutes
271
- }
272
-
273
- # Protected logout endpoint
274
- @api.post("/auth/logout", auth=[auth], guards=[IsAuthenticated()])
275
- async def logout(request):
276
- """Logout - revoke current token."""
277
- jti = request["context"]["auth_claims"]["jti"]
278
-
279
- # Calculate TTL from token expiry
280
- exp = request["context"]["auth_claims"]["exp"]
281
- import time
282
- ttl = max(0, exp - int(time.time()))
283
-
284
- # Revoke token
285
- await revocation.revoke(jti, ttl=ttl)
286
-
287
- return {"message": "Logged out successfully"}
288
-
289
- # Refresh token endpoint
290
- @api.post("/auth/refresh", guards=[AllowAny()])
291
- async def refresh_access_token(refresh_token: str):
292
- """Exchange refresh token for new access token."""
293
- try:
294
- # Decode refresh token
295
- token = Token.decode(refresh_token, secret=settings.SECRET_KEY)
296
-
297
- # Validate it's a refresh token
298
- if token.extras.get("type") != "refresh":
299
- return {"error": "Invalid token type"}, 401
300
-
301
- # Check if revoked
302
- if await revocation.is_revoked(token.jti):
303
- return {"error": "Token has been revoked"}, 401
304
-
305
- # Create new access token
306
- new_access = Token.create(
307
- sub=token.sub,
308
- expires_delta=timedelta(minutes=15),
309
- jti=str(uuid.uuid4()),
310
- )
311
-
312
- return {
313
- "access_token": new_access.encode(settings.SECRET_KEY),
314
- "token_type": "bearer",
315
- "expires_in": 900
316
- }
317
-
318
- except ValueError as e:
319
- return {"error": "Invalid token"}, 401
320
-
321
- # Protected endpoint
322
- @api.get("/profile", auth=[auth], guards=[IsAuthenticated()])
323
- async def get_profile(request):
324
- """Get current user profile."""
325
- user_id = request["context"]["user_id"]
326
- is_admin = request["context"]["is_admin"]
327
- permissions = request["context"].get("permissions", [])
328
-
329
- return {
330
- "user_id": user_id,
331
- "is_admin": is_admin,
332
- "permissions": permissions,
333
- }
334
- ```
335
-
336
- ---
337
-
338
- ## Best Practices
339
-
340
- ### 1. Always Use JTI with Revocation
341
-
342
- ```python
343
- # ✅ Good - JTI for revocation
344
- token = Token.create(
345
- sub="user123",
346
- jti=str(uuid.uuid4()), # Unique ID
347
- expires_delta=timedelta(hours=1)
348
- )
349
-
350
- # ❌ Bad - No JTI, can't revoke
351
- token = Token.create(
352
- sub="user123",
353
- expires_delta=timedelta(hours=1)
354
- )
355
- ```
356
-
357
- ### 2. Set Appropriate TTL
358
-
359
- ```python
360
- # Match revocation TTL to token expiry
361
- token = Token.create(sub="user123", expires_delta=timedelta(hours=1))
362
-
363
- # Revoke with same TTL (3600 seconds = 1 hour)
364
- await revocation.revoke(jti, ttl=3600)
365
-
366
- # After 1 hour, token expires AND revocation entry is cleaned up
367
- ```
368
-
369
- ### 3. Use DjangoCacheRevocation in Production
370
-
371
- ```python
372
- # ✅ Recommended for production
373
- revocation = DjangoCacheRevocation() # Uses Redis/Memcached
374
-
375
- # ⚠️ Only for development
376
- revocation = InMemoryRevocation() # Single-process only
377
-
378
- # 🐌 Slower but works without cache
379
- revocation = DjangoORMRevocation(model='myapp.RevokedToken')
380
- ```
381
-
382
- ---
383
-
384
- ## Summary
385
-
386
- - **Revocation is OPTIONAL** - only use if you need logout
387
- - **Multiple storage options** - In-memory, Cache, Database, Custom
388
- - **No Redis requirement** - Works with Django cache (any backend)
389
- - **Performance-aware** - Cache-based revocation still does ~50k RPS
390
- - **JTI auto-required** - When revocation is enabled, JTI becomes mandatory
391
- - **Flexible** - Bring your own storage with custom handler
File without changes
@@ -1 +0,0 @@
1
- # Admin integration tests
@@ -1,6 +0,0 @@
1
- """
2
- Pytest configuration for admin integration tests.
3
-
4
- The parent conftest will auto-detect admin_tests in the path
5
- and configure Django with admin apps enabled.
6
- """
@@ -1,278 +0,0 @@
1
- """
2
- Tests for Django admin integration that actually use a Django project.
3
-
4
- These tests configure Django properly and will FAIL if ASGI bridge is broken.
5
- """
6
-
7
- import pytest
8
- from django_bolt.api import BoltAPI
9
- from django_bolt.testing import TestClient
10
-
11
-
12
- @pytest.fixture(scope="module")
13
- def api_with_admin():
14
- """Create API with admin enabled using real Django project."""
15
- api = BoltAPI()
16
- api._register_admin_routes('127.0.0.1', 8000)
17
-
18
- @api.get("/test")
19
- async def test_route():
20
- return {"test": "ok"}
21
-
22
- return api
23
-
24
-
25
- @pytest.fixture(scope="module")
26
- def client(api_with_admin):
27
- """Create test client with HTTP layer."""
28
- with TestClient(api_with_admin, use_http_layer=True) as client:
29
- yield client
30
-
31
-
32
- def test_admin_root_redirect(client):
33
- """Test /admin/ returns content (redirect or login page)."""
34
- response = client.get("/admin/")
35
-
36
- print(f"\n[Admin Root Test]")
37
- print(f"Status: {response.status_code}")
38
- print(f"Headers: {dict(response.headers)}")
39
- print(f"Body length: {len(response.content)}")
40
- print(f"Body preview: {response.text[:300] if response.text else 'N/A'}")
41
-
42
- # Should return a valid response (redirect or login page)
43
- assert response.status_code in (200, 301, 302), f"Expected valid response, got {response.status_code}"
44
-
45
- # CRITICAL: Body should NOT be empty
46
- assert len(response.content) > 0, f"Response body is EMPTY! Got {len(response.content)} bytes. ASGI bridge is BROKEN!"
47
-
48
-
49
- def test_admin_login_page(client):
50
- """Test /admin/login/ returns HTML page (not empty body)."""
51
- response = client.get("/admin/login/")
52
-
53
- print(f"\n[Admin Login Test]")
54
- print(f"Status: {response.status_code}")
55
- print(f"Headers: {dict(response.headers)}")
56
- print(f"Body length: {len(response.content)}")
57
- print(f"Body preview: {response.text[:300]}")
58
-
59
- # Should return 200 OK
60
- assert response.status_code == 200, f"Expected 200, got {response.status_code}"
61
-
62
- # CRITICAL: Body should NOT be empty - THIS IS THE BUG
63
- assert len(response.content) > 0, f"Admin login page body is EMPTY! Got {len(response.content)} bytes. ASGI bridge is BROKEN!"
64
-
65
- # Should be HTML
66
- content_type = response.headers.get('content-type', '')
67
- assert 'html' in content_type.lower(), f"Expected HTML, got {content_type}"
68
-
69
- # Should contain login form
70
- body_text = response.text.lower()
71
- assert 'login' in body_text or 'django' in body_text, f"Expected login content, got: {body_text[:200]}"
72
-
73
-
74
- @pytest.mark.django_db
75
- def test_asgi_bridge_direct_with_real_django():
76
- """Test ASGI bridge directly with real Django configuration."""
77
- from django_bolt.admin.asgi_bridge import ASGIFallbackHandler
78
- import asyncio
79
-
80
- # Database is already set up by pytest-django
81
- handler = ASGIFallbackHandler(server_host="127.0.0.1", server_port=8000)
82
-
83
- request = {
84
- "method": "GET",
85
- "path": "/admin/login/",
86
- "body": b"",
87
- "params": {},
88
- "query": {},
89
- "headers": {"host": "127.0.0.1:8000"},
90
- "cookies": {},
91
- "context": None,
92
- }
93
-
94
- status, headers, body = asyncio.run(handler.handle_request(request))
95
-
96
- print(f"\n[ASGI Bridge Direct Test]")
97
- print(f"Status: {status}")
98
- print(f"Headers: {dict(headers)}")
99
- print(f"Body length: {len(body)}")
100
- print(f"Body preview: {body[:300]}")
101
-
102
- # Validate structure
103
- assert isinstance(status, int), f"Status should be int, got {type(status)}"
104
- assert isinstance(headers, list), f"Headers should be list, got {type(headers)}"
105
- assert isinstance(body, bytes), f"Body should be bytes, got {type(body)}"
106
-
107
- # Should return 200 OK
108
- assert status == 200, f"Expected 200, got {status}"
109
-
110
- # CRITICAL TEST: Body should NOT be empty - THIS WILL FAIL IF BUG EXISTS
111
- assert len(body) > 0, f"ASGI bridge returned EMPTY body! Expected HTML content. Body length: {len(body)}"
112
-
113
- # Should be HTML content
114
- body_text = body.decode('utf-8', errors='ignore')
115
- assert 'html' in body_text.lower(), f"Expected HTML content, got: {body_text[:100]}"
116
- assert 'django' in body_text.lower() or 'login' in body_text.lower(), f"Expected Django admin content"
117
-
118
-
119
- @pytest.mark.django_db
120
- def test_asgi_bridge_admin_root():
121
- """Test ASGI bridge handles /admin/ root correctly."""
122
- from django_bolt.admin.asgi_bridge import ASGIFallbackHandler
123
- import asyncio
124
-
125
- handler = ASGIFallbackHandler(server_host="127.0.0.1", server_port=8000)
126
-
127
- request = {
128
- "method": "GET",
129
- "path": "/admin/",
130
- "body": b"",
131
- "params": {},
132
- "query": {},
133
- "headers": {"host": "127.0.0.1:8000"},
134
- "cookies": {},
135
- "context": None,
136
- }
137
-
138
- status, headers, body = asyncio.run(handler.handle_request(request))
139
-
140
- # Should redirect to login
141
- assert status in (301, 302), f"Expected redirect, got {status}"
142
-
143
- # Should have location header
144
- location = None
145
- for name, value in headers:
146
- if name.lower() == 'location':
147
- location = value
148
- break
149
-
150
- assert location is not None, "Redirect should have Location header"
151
- assert '/admin/login/' in location, f"Should redirect to login, got {location}"
152
-
153
-
154
- @pytest.mark.django_db
155
- def test_asgi_bridge_with_query_params():
156
- """Test ASGI bridge handles query parameters correctly."""
157
- from django_bolt.admin.asgi_bridge import ASGIFallbackHandler
158
- import asyncio
159
-
160
- handler = ASGIFallbackHandler(server_host="127.0.0.1", server_port=8000)
161
-
162
- request = {
163
- "method": "GET",
164
- "path": "/admin/login/",
165
- "body": b"",
166
- "params": {},
167
- "query": {"next": "/admin/"},
168
- "headers": {"host": "127.0.0.1:8000"},
169
- "cookies": {},
170
- "context": None,
171
- }
172
-
173
- status, headers, body = asyncio.run(handler.handle_request(request))
174
-
175
- # Should return 200 OK
176
- assert status == 200, f"Expected 200, got {status}"
177
- assert len(body) > 0, "Body should not be empty"
178
-
179
-
180
- @pytest.mark.django_db
181
- def test_asgi_bridge_post_request():
182
- """Test ASGI bridge handles POST requests correctly."""
183
- from django_bolt.admin.asgi_bridge import ASGIFallbackHandler
184
- import asyncio
185
-
186
- handler = ASGIFallbackHandler(server_host="127.0.0.1", server_port=8000)
187
-
188
- # POST request with form data
189
- form_data = b"username=admin&password=test123"
190
-
191
- request = {
192
- "method": "POST",
193
- "path": "/admin/login/",
194
- "body": form_data,
195
- "params": {},
196
- "query": {},
197
- "headers": {
198
- "host": "127.0.0.1:8000",
199
- "content-type": "application/x-www-form-urlencoded",
200
- "content-length": str(len(form_data)),
201
- },
202
- "cookies": {},
203
- "context": None,
204
- }
205
-
206
- status, headers, body = asyncio.run(handler.handle_request(request))
207
-
208
- # Should return response (even if login fails, it should process the request)
209
- assert isinstance(status, int), f"Status should be int, got {type(status)}"
210
- assert len(body) > 0, "Body should not be empty"
211
-
212
-
213
- @pytest.mark.django_db
214
- def test_asgi_bridge_404_path():
215
- """Test ASGI bridge handles non-existent admin paths correctly."""
216
- from django_bolt.admin.asgi_bridge import ASGIFallbackHandler
217
- import asyncio
218
-
219
- handler = ASGIFallbackHandler(server_host="127.0.0.1", server_port=8000)
220
-
221
- request = {
222
- "method": "GET",
223
- "path": "/admin/nonexistent/path/",
224
- "body": b"",
225
- "params": {},
226
- "query": {},
227
- "headers": {"host": "127.0.0.1:8000"},
228
- "cookies": {},
229
- "context": None,
230
- }
231
-
232
- status, headers, body = asyncio.run(handler.handle_request(request))
233
-
234
- # Django redirects unauthenticated users to login for non-existent admin paths
235
- # This is expected Django admin behavior
236
- assert status in (302, 404), f"Expected redirect or 404, got {status}"
237
- assert len(body) >= 0, "Response should have structure"
238
-
239
-
240
- @pytest.mark.django_db
241
- def test_asgi_bridge_with_cookies():
242
- """Test ASGI bridge handles cookies correctly."""
243
- from django_bolt.admin.asgi_bridge import ASGIFallbackHandler
244
- import asyncio
245
-
246
- handler = ASGIFallbackHandler(server_host="127.0.0.1", server_port=8000)
247
-
248
- request = {
249
- "method": "GET",
250
- "path": "/admin/login/",
251
- "body": b"",
252
- "params": {},
253
- "query": {},
254
- "headers": {
255
- "host": "127.0.0.1:8000",
256
- "cookie": "sessionid=abc123; csrftoken=xyz789",
257
- },
258
- "cookies": {
259
- "sessionid": "abc123",
260
- "csrftoken": "xyz789",
261
- },
262
- "context": None,
263
- }
264
-
265
- status, headers, body = asyncio.run(handler.handle_request(request))
266
-
267
- # Should return 200 OK
268
- assert status == 200, f"Expected 200, got {status}"
269
- assert len(body) > 0, "Body should not be empty"
270
-
271
- # Should set CSRF token cookie
272
- has_csrf = False
273
- for name, value in headers:
274
- if name.lower() == 'set-cookie' and 'csrftoken' in value:
275
- has_csrf = True
276
- break
277
-
278
- assert has_csrf, "Response should include CSRF token cookie"
@@ -1,9 +0,0 @@
1
- """
2
- URL configuration for admin integration tests.
3
- """
4
- from django.contrib import admin
5
- from django.urls import path
6
-
7
- urlpatterns = [
8
- path('admin/', admin.site.urls),
9
- ]
File without changes