django-bolt 0.1.0__cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-bolt might be problematic. Click here for more details.
- django_bolt/__init__.py +147 -0
- django_bolt/_core.abi3.so +0 -0
- django_bolt/admin/__init__.py +25 -0
- django_bolt/admin/admin_detection.py +179 -0
- django_bolt/admin/asgi_bridge.py +267 -0
- django_bolt/admin/routes.py +91 -0
- django_bolt/admin/static.py +155 -0
- django_bolt/admin/static_routes.py +111 -0
- django_bolt/api.py +1011 -0
- django_bolt/apps.py +7 -0
- django_bolt/async_collector.py +228 -0
- django_bolt/auth/README.md +464 -0
- django_bolt/auth/REVOCATION_EXAMPLE.md +391 -0
- django_bolt/auth/__init__.py +84 -0
- django_bolt/auth/backends.py +236 -0
- django_bolt/auth/guards.py +224 -0
- django_bolt/auth/jwt_utils.py +212 -0
- django_bolt/auth/revocation.py +286 -0
- django_bolt/auth/token.py +335 -0
- django_bolt/binding.py +363 -0
- django_bolt/bootstrap.py +77 -0
- django_bolt/cli.py +133 -0
- django_bolt/compression.py +104 -0
- django_bolt/decorators.py +159 -0
- django_bolt/dependencies.py +128 -0
- django_bolt/error_handlers.py +305 -0
- django_bolt/exceptions.py +294 -0
- django_bolt/health.py +129 -0
- django_bolt/logging/__init__.py +6 -0
- django_bolt/logging/config.py +357 -0
- django_bolt/logging/middleware.py +296 -0
- django_bolt/management/__init__.py +1 -0
- django_bolt/management/commands/__init__.py +0 -0
- django_bolt/management/commands/runbolt.py +427 -0
- django_bolt/middleware/__init__.py +32 -0
- django_bolt/middleware/compiler.py +131 -0
- django_bolt/middleware/middleware.py +247 -0
- django_bolt/openapi/__init__.py +23 -0
- django_bolt/openapi/config.py +196 -0
- django_bolt/openapi/plugins.py +439 -0
- django_bolt/openapi/routes.py +152 -0
- django_bolt/openapi/schema_generator.py +581 -0
- django_bolt/openapi/spec/__init__.py +68 -0
- django_bolt/openapi/spec/base.py +74 -0
- django_bolt/openapi/spec/callback.py +24 -0
- django_bolt/openapi/spec/components.py +72 -0
- django_bolt/openapi/spec/contact.py +21 -0
- django_bolt/openapi/spec/discriminator.py +25 -0
- django_bolt/openapi/spec/encoding.py +67 -0
- django_bolt/openapi/spec/enums.py +41 -0
- django_bolt/openapi/spec/example.py +36 -0
- django_bolt/openapi/spec/external_documentation.py +21 -0
- django_bolt/openapi/spec/header.py +132 -0
- django_bolt/openapi/spec/info.py +50 -0
- django_bolt/openapi/spec/license.py +28 -0
- django_bolt/openapi/spec/link.py +66 -0
- django_bolt/openapi/spec/media_type.py +51 -0
- django_bolt/openapi/spec/oauth_flow.py +36 -0
- django_bolt/openapi/spec/oauth_flows.py +28 -0
- django_bolt/openapi/spec/open_api.py +87 -0
- django_bolt/openapi/spec/operation.py +105 -0
- django_bolt/openapi/spec/parameter.py +147 -0
- django_bolt/openapi/spec/path_item.py +78 -0
- django_bolt/openapi/spec/paths.py +27 -0
- django_bolt/openapi/spec/reference.py +38 -0
- django_bolt/openapi/spec/request_body.py +38 -0
- django_bolt/openapi/spec/response.py +48 -0
- django_bolt/openapi/spec/responses.py +44 -0
- django_bolt/openapi/spec/schema.py +678 -0
- django_bolt/openapi/spec/security_requirement.py +28 -0
- django_bolt/openapi/spec/security_scheme.py +69 -0
- django_bolt/openapi/spec/server.py +34 -0
- django_bolt/openapi/spec/server_variable.py +32 -0
- django_bolt/openapi/spec/tag.py +32 -0
- django_bolt/openapi/spec/xml.py +44 -0
- django_bolt/pagination.py +669 -0
- django_bolt/param_functions.py +49 -0
- django_bolt/params.py +337 -0
- django_bolt/request_parsing.py +128 -0
- django_bolt/responses.py +214 -0
- django_bolt/router.py +48 -0
- django_bolt/serialization.py +193 -0
- django_bolt/status_codes.py +321 -0
- django_bolt/testing/__init__.py +10 -0
- django_bolt/testing/client.py +274 -0
- django_bolt/testing/helpers.py +93 -0
- django_bolt/tests/__init__.py +0 -0
- django_bolt/tests/admin_tests/__init__.py +1 -0
- django_bolt/tests/admin_tests/conftest.py +6 -0
- django_bolt/tests/admin_tests/test_admin_with_django.py +278 -0
- django_bolt/tests/admin_tests/urls.py +9 -0
- django_bolt/tests/cbv/__init__.py +0 -0
- django_bolt/tests/cbv/test_class_views.py +570 -0
- django_bolt/tests/cbv/test_class_views_django_orm.py +703 -0
- django_bolt/tests/cbv/test_class_views_features.py +1173 -0
- django_bolt/tests/cbv/test_class_views_with_client.py +622 -0
- django_bolt/tests/conftest.py +165 -0
- django_bolt/tests/test_action_decorator.py +399 -0
- django_bolt/tests/test_auth_secret_key.py +83 -0
- django_bolt/tests/test_decorator_syntax.py +159 -0
- django_bolt/tests/test_error_handling.py +481 -0
- django_bolt/tests/test_file_response.py +192 -0
- django_bolt/tests/test_global_cors.py +172 -0
- django_bolt/tests/test_guards_auth.py +441 -0
- django_bolt/tests/test_guards_integration.py +303 -0
- django_bolt/tests/test_health.py +283 -0
- django_bolt/tests/test_integration_validation.py +400 -0
- django_bolt/tests/test_json_validation.py +536 -0
- django_bolt/tests/test_jwt_auth.py +327 -0
- django_bolt/tests/test_jwt_token.py +458 -0
- django_bolt/tests/test_logging.py +837 -0
- django_bolt/tests/test_logging_merge.py +419 -0
- django_bolt/tests/test_middleware.py +492 -0
- django_bolt/tests/test_middleware_server.py +230 -0
- django_bolt/tests/test_model_viewset.py +323 -0
- django_bolt/tests/test_models.py +24 -0
- django_bolt/tests/test_pagination.py +1258 -0
- django_bolt/tests/test_parameter_validation.py +178 -0
- django_bolt/tests/test_syntax.py +626 -0
- django_bolt/tests/test_testing_utilities.py +163 -0
- django_bolt/tests/test_testing_utilities_simple.py +123 -0
- django_bolt/tests/test_viewset_unified.py +346 -0
- django_bolt/typing.py +273 -0
- django_bolt/views.py +1110 -0
- django_bolt-0.1.0.dist-info/METADATA +629 -0
- django_bolt-0.1.0.dist-info/RECORD +128 -0
- django_bolt-0.1.0.dist-info/WHEEL +4 -0
- 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,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"
|
|
File without changes
|