django-bolt 0.1.0__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 (128) hide show
  1. django_bolt/__init__.py +147 -0
  2. django_bolt/_core.pyd +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,274 @@
1
+ """Test clients for django-bolt using per-instance test state.
2
+
3
+ This version uses the test_state.rs infrastructure which provides:
4
+ - Per-instance routers (no global state conflicts)
5
+ - Per-instance event loops (proper async handling)
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ import httpx
12
+ from httpx import Response
13
+
14
+ from django_bolt import BoltAPI
15
+
16
+
17
+ class BoltTestTransport(httpx.BaseTransport):
18
+ """HTTP transport that routes requests through django-bolt's per-instance test handler.
19
+
20
+ Args:
21
+ app_id: Test app instance ID
22
+ raise_server_exceptions: If True, raise exceptions from handlers
23
+ use_http_layer: If True, route through Actix HTTP layer (enables testing of
24
+ middleware like CORS, rate limiting, compression). If False (default),
25
+ use fast direct dispatch for unit tests.
26
+ """
27
+
28
+ def __init__(self, app_id: int, raise_server_exceptions: bool = True, use_http_layer: bool = False):
29
+ self.app_id = app_id
30
+ self.raise_server_exceptions = raise_server_exceptions
31
+ self.use_http_layer = use_http_layer
32
+
33
+ def handle_request(self, request: httpx.Request) -> httpx.Response:
34
+ """Handle a request by routing it through Rust."""
35
+ from django_bolt import _core
36
+
37
+ # Parse URL
38
+ url = request.url
39
+ path = url.path
40
+ query_string = url.query.decode('utf-8') if url.query else None
41
+
42
+ # Extract headers
43
+ headers = [(k.decode('utf-8'), v.decode('utf-8')) for k, v in request.headers.raw]
44
+
45
+ # Get body
46
+ # Check if content has been read already
47
+ if hasattr(request, "_content"):
48
+ body_bytes = request.content
49
+ else:
50
+ # For streaming/multipart requests, need to read the content first
51
+ try:
52
+ # Try to read the request stream
53
+ if hasattr(request.stream, 'read'):
54
+ body_bytes = request.stream.read()
55
+ else:
56
+ # Fall back to iterating the stream
57
+ body_bytes = b''.join(request.stream)
58
+ except Exception:
59
+ # Last resort: try to get content directly
60
+ body_bytes = request.content if hasattr(request, "_content") else b''
61
+
62
+ # Get method
63
+ method = request.method
64
+
65
+ try:
66
+ # Choose handler based on mode
67
+ if self.use_http_layer:
68
+ # Route through Actix HTTP layer (for middleware testing)
69
+ status_code, resp_headers, resp_body = _core.handle_actix_http_request(
70
+ app_id=self.app_id,
71
+ method=method,
72
+ path=path,
73
+ headers=headers,
74
+ body=body_bytes,
75
+ query_string=query_string,
76
+ )
77
+ else:
78
+ # Fast direct dispatch (for unit tests)
79
+ status_code, resp_headers, resp_body = _core.handle_test_request_for(
80
+ app_id=self.app_id,
81
+ method=method,
82
+ path=path,
83
+ headers=headers,
84
+ body=body_bytes,
85
+ query_string=query_string,
86
+ )
87
+
88
+ # Build httpx Response
89
+ return Response(
90
+ status_code=status_code,
91
+ headers=resp_headers,
92
+ content=resp_body,
93
+ request=request,
94
+ )
95
+
96
+ except Exception as e:
97
+ if self.raise_server_exceptions:
98
+ raise
99
+ # Return 500 error
100
+ return Response(
101
+ status_code=500,
102
+ headers=[('content-type', 'text/plain')],
103
+ content=f"Test client error: {e}".encode('utf-8'),
104
+ request=request,
105
+ )
106
+
107
+
108
+ class TestClient(httpx.Client):
109
+ """Synchronous test client for django-bolt using per-instance test state.
110
+
111
+ This client:
112
+ - Creates an isolated test app instance (no global state conflicts)
113
+ - Manages its own event loop for async handlers
114
+ - Routes through full Rust pipeline (auth, middleware, compression)
115
+ - Can run multiple tests in parallel without conflicts
116
+
117
+ Usage:
118
+ api = BoltAPI()
119
+
120
+ @api.get("/hello")
121
+ async def hello():
122
+ return {"message": "world"}
123
+
124
+ with TestClient(api) as client:
125
+ response = client.get("/hello")
126
+ assert response.status_code == 200
127
+ assert response.json() == {"message": "world"}
128
+ """
129
+
130
+ __test__ = False # Tell pytest this is not a test class
131
+
132
+ def __init__(
133
+ self,
134
+ api: BoltAPI,
135
+ base_url: str = "http://testserver.local",
136
+ raise_server_exceptions: bool = True,
137
+ use_http_layer: bool = False,
138
+ cors_allowed_origins: list[str] | None = None,
139
+ **kwargs: Any,
140
+ ):
141
+ """Initialize test client.
142
+
143
+ Args:
144
+ api: BoltAPI instance to test
145
+ base_url: Base URL for requests
146
+ raise_server_exceptions: If True, raise exceptions from handlers
147
+ use_http_layer: If True, route through Actix HTTP layer (enables testing
148
+ CORS, rate limiting, compression). Default False for fast tests.
149
+ cors_allowed_origins: Global CORS allowed origins for testing
150
+ **kwargs: Additional arguments passed to httpx.Client
151
+ """
152
+ from django_bolt import _core
153
+
154
+ # Create test app instance
155
+ self.app_id = _core.create_test_app(api._dispatch, False, cors_allowed_origins)
156
+
157
+ # Register routes
158
+ rust_routes = [
159
+ (method, path, handler_id, handler)
160
+ for method, path, handler_id, handler in api._routes
161
+ ]
162
+ _core.register_test_routes(self.app_id, rust_routes)
163
+
164
+ # Register middleware metadata if any exists
165
+ if api._handler_middleware:
166
+ middleware_data = [
167
+ (handler_id, meta)
168
+ for handler_id, meta in api._handler_middleware.items()
169
+ ]
170
+ _core.register_test_middleware_metadata(self.app_id, middleware_data)
171
+
172
+ # Ensure runtime is ready
173
+ _core.ensure_test_runtime(self.app_id)
174
+
175
+ super().__init__(
176
+ base_url=base_url,
177
+ transport=BoltTestTransport(self.app_id, raise_server_exceptions, use_http_layer),
178
+ follow_redirects=True,
179
+ **kwargs,
180
+ )
181
+ self.api = api
182
+
183
+ def __enter__(self):
184
+ """Enter context manager."""
185
+ return super().__enter__()
186
+
187
+ def __exit__(self, *args):
188
+ """Exit context manager and cleanup test app."""
189
+ from django_bolt import _core
190
+
191
+ try:
192
+ _core.destroy_test_app(self.app_id)
193
+ except:
194
+ pass
195
+ return super().__exit__(*args)
196
+
197
+
198
+ class AsyncTestClient(httpx.AsyncClient):
199
+ """Asynchronous test client for django-bolt using per-instance test state."""
200
+
201
+ __test__ = False # Tell pytest this is not a test class
202
+
203
+ def __init__(
204
+ self,
205
+ api: BoltAPI,
206
+ base_url: str = "http://testserver.local",
207
+ raise_server_exceptions: bool = True,
208
+ use_http_layer: bool = False,
209
+ cors_allowed_origins: list[str] | None = None,
210
+ **kwargs: Any,
211
+ ):
212
+ """Initialize async test client.
213
+
214
+ Args:
215
+ api: BoltAPI instance to test
216
+ base_url: Base URL for requests
217
+ raise_server_exceptions: If True, raise exceptions from handlers
218
+ use_http_layer: If True, route through Actix HTTP layer (enables testing
219
+ CORS, rate limiting, compression). Default False for fast tests.
220
+ cors_allowed_origins: Global CORS allowed origins for testing
221
+ **kwargs: Additional arguments passed to httpx.AsyncClient
222
+ """
223
+ from django_bolt import _core
224
+
225
+ # Create test app instance
226
+ self.app_id = _core.create_test_app(api._dispatch, False, cors_allowed_origins)
227
+
228
+ # Register routes
229
+ rust_routes = [
230
+ (method, path, handler_id, handler)
231
+ for method, path, handler_id, handler in api._routes
232
+ ]
233
+ _core.register_test_routes(self.app_id, rust_routes)
234
+
235
+ # Register middleware metadata if any exists
236
+ if api._handler_middleware:
237
+ middleware_data = [
238
+ (handler_id, meta)
239
+ for handler_id, meta in api._handler_middleware.items()
240
+ ]
241
+ _core.register_test_middleware_metadata(self.app_id, middleware_data)
242
+
243
+ # Ensure runtime is ready
244
+ _core.ensure_test_runtime(self.app_id)
245
+
246
+ # Create async transport
247
+ class AsyncTransport(httpx.AsyncBaseTransport):
248
+ def __init__(self, app_id: int, raise_exceptions: bool, use_http_layer: bool):
249
+ self._sync_transport = BoltTestTransport(app_id, raise_exceptions, use_http_layer)
250
+
251
+ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
252
+ return self._sync_transport.handle_request(request)
253
+
254
+ super().__init__(
255
+ base_url=base_url,
256
+ transport=AsyncTransport(self.app_id, raise_server_exceptions, use_http_layer),
257
+ follow_redirects=True,
258
+ **kwargs,
259
+ )
260
+ self.api = api
261
+
262
+ async def __aenter__(self):
263
+ """Enter async context manager."""
264
+ return await super().__aenter__()
265
+
266
+ async def __aexit__(self, *args):
267
+ """Exit async context manager and cleanup test app."""
268
+ from django_bolt import _core
269
+
270
+ try:
271
+ _core.destroy_test_app(self.app_id)
272
+ except:
273
+ pass
274
+ return await super().__aexit__(*args)
@@ -0,0 +1,93 @@
1
+ """Helper functions for creating test clients."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from django_bolt import BoltAPI
7
+ from django_bolt.testing.client import AsyncTestClient, TestClient
8
+
9
+
10
+ def create_test_client(
11
+ api: BoltAPI,
12
+ base_url: str = "http://testserver.local",
13
+ raise_server_exceptions: bool = True,
14
+ bootstrap_django: bool = True,
15
+ **kwargs: Any,
16
+ ) -> TestClient:
17
+ """Create a synchronous test client.
18
+
19
+ Args:
20
+ api: BoltAPI instance to test
21
+ base_url: Base URL for requests
22
+ raise_server_exceptions: If True, raise handler exceptions instead of 500 responses
23
+ **kwargs: Additional httpx.Client arguments
24
+
25
+ Returns:
26
+ TestClient instance
27
+
28
+ Example:
29
+ from django_bolt import BoltAPI
30
+ from django_bolt.testing import create_test_client
31
+
32
+ api = BoltAPI()
33
+
34
+ @api.get("/users/{user_id}")
35
+ async def get_user(user_id: int):
36
+ return {"id": user_id, "name": "Test User"}
37
+
38
+ def test_get_user():
39
+ with create_test_client(api) as client:
40
+ response = client.get("/users/123")
41
+ assert response.status_code == 200
42
+ assert response.json()["id"] == 123
43
+ """
44
+ return TestClient(
45
+ api=api,
46
+ base_url=base_url,
47
+ raise_server_exceptions=raise_server_exceptions,
48
+ bootstrap_django=bootstrap_django,
49
+ **kwargs,
50
+ )
51
+
52
+
53
+ def create_async_test_client(
54
+ api: BoltAPI,
55
+ base_url: str = "http://testserver.local",
56
+ raise_server_exceptions: bool = True,
57
+ bootstrap_django: bool = True,
58
+ **kwargs: Any,
59
+ ) -> AsyncTestClient:
60
+ """Create an asynchronous test client.
61
+
62
+ Args:
63
+ api: BoltAPI instance to test
64
+ base_url: Base URL for requests
65
+ raise_server_exceptions: If True, raise handler exceptions instead of 500 responses
66
+ **kwargs: Additional httpx.AsyncClient arguments
67
+
68
+ Returns:
69
+ AsyncTestClient instance
70
+
71
+ Example:
72
+ from django_bolt import BoltAPI
73
+ from django_bolt.testing import create_async_test_client
74
+
75
+ api = BoltAPI()
76
+
77
+ @api.get("/users/{user_id}")
78
+ async def get_user(user_id: int):
79
+ return {"id": user_id, "name": "Test User"}
80
+
81
+ async def test_get_user():
82
+ async with create_async_test_client(api) as client:
83
+ response = await client.get("/users/123")
84
+ assert response.status_code == 200
85
+ assert response.json()["id"] == 123
86
+ """
87
+ return AsyncTestClient(
88
+ api=api,
89
+ base_url=base_url,
90
+ raise_server_exceptions=raise_server_exceptions,
91
+ bootstrap_django=bootstrap_django,
92
+ **kwargs,
93
+ )
File without changes
@@ -0,0 +1 @@
1
+ # Admin integration tests
@@ -0,0 +1,6 @@
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
+ """
@@ -0,0 +1,278 @@
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"
@@ -0,0 +1,9 @@
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