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.
- django_bolt/__init__.py +147 -0
- django_bolt/_core.pyd +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,441 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test guards and authentication system for Django-Bolt.
|
|
3
|
+
|
|
4
|
+
Tests the new DRF-inspired guards and authentication classes.
|
|
5
|
+
"""
|
|
6
|
+
import jwt
|
|
7
|
+
import time
|
|
8
|
+
import pytest
|
|
9
|
+
from django_bolt import BoltAPI
|
|
10
|
+
from django_bolt.auth import (
|
|
11
|
+
JWTAuthentication,
|
|
12
|
+
APIKeyAuthentication,
|
|
13
|
+
AuthContext,
|
|
14
|
+
AllowAny,
|
|
15
|
+
IsAuthenticated,
|
|
16
|
+
IsAdminUser,
|
|
17
|
+
IsStaff,
|
|
18
|
+
HasPermission,
|
|
19
|
+
HasAnyPermission,
|
|
20
|
+
HasAllPermissions
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestAuthenticationClasses:
|
|
25
|
+
"""Test authentication class configurations"""
|
|
26
|
+
|
|
27
|
+
def test_jwt_authentication_basic(self):
|
|
28
|
+
"""Test JWT authentication class initialization"""
|
|
29
|
+
auth = JWTAuthentication(secret="test-secret")
|
|
30
|
+
assert auth.secret == "test-secret"
|
|
31
|
+
assert auth.algorithms == ["HS256"]
|
|
32
|
+
assert auth.scheme_name == "jwt"
|
|
33
|
+
|
|
34
|
+
metadata = auth.to_metadata()
|
|
35
|
+
assert metadata["type"] == "jwt"
|
|
36
|
+
assert metadata["secret"] == "test-secret"
|
|
37
|
+
assert metadata["algorithms"] == ["HS256"]
|
|
38
|
+
|
|
39
|
+
def test_jwt_authentication_with_options(self):
|
|
40
|
+
"""Test JWT authentication with all options"""
|
|
41
|
+
auth = JWTAuthentication(
|
|
42
|
+
secret="my-secret",
|
|
43
|
+
algorithms=["HS256", "HS384"],
|
|
44
|
+
header="x-auth-token",
|
|
45
|
+
audience="my-app",
|
|
46
|
+
issuer="auth-server"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
metadata = auth.to_metadata()
|
|
50
|
+
assert metadata["secret"] == "my-secret"
|
|
51
|
+
assert metadata["algorithms"] == ["HS256", "HS384"]
|
|
52
|
+
assert metadata["header"] == "x-auth-token"
|
|
53
|
+
assert metadata["audience"] == "my-app"
|
|
54
|
+
assert metadata["issuer"] == "auth-server"
|
|
55
|
+
|
|
56
|
+
def test_api_key_authentication(self):
|
|
57
|
+
"""Test API key authentication class"""
|
|
58
|
+
auth = APIKeyAuthentication(
|
|
59
|
+
api_keys={"key1", "key2", "key3"},
|
|
60
|
+
header="x-api-key"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
assert auth.scheme_name == "api_key"
|
|
64
|
+
metadata = auth.to_metadata()
|
|
65
|
+
assert metadata["type"] == "api_key"
|
|
66
|
+
assert set(metadata["api_keys"]) == {"key1", "key2", "key3"}
|
|
67
|
+
assert metadata["header"] == "x-api-key"
|
|
68
|
+
|
|
69
|
+
def test_api_key_with_permissions(self):
|
|
70
|
+
"""Test API key authentication with permission mapping"""
|
|
71
|
+
auth = APIKeyAuthentication(
|
|
72
|
+
api_keys={"admin-key", "read-key"},
|
|
73
|
+
key_permissions={
|
|
74
|
+
"admin-key": ["users.create", "users.delete"],
|
|
75
|
+
"read-key": ["users.view"]
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
metadata = auth.to_metadata()
|
|
80
|
+
assert "admin-key" in metadata["key_permissions"]
|
|
81
|
+
assert "users.create" in metadata["key_permissions"]["admin-key"]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestPermissionGuards:
|
|
85
|
+
"""Test permission guard classes"""
|
|
86
|
+
|
|
87
|
+
def test_allow_any(self):
|
|
88
|
+
"""Test AllowAny guard"""
|
|
89
|
+
guard = AllowAny()
|
|
90
|
+
assert guard.guard_name == "allow_any"
|
|
91
|
+
assert guard.to_metadata() == {"type": "allow_any"}
|
|
92
|
+
|
|
93
|
+
# Should allow without auth context
|
|
94
|
+
assert guard.has_permission(None) == True
|
|
95
|
+
|
|
96
|
+
def test_is_authenticated(self):
|
|
97
|
+
"""Test IsAuthenticated guard"""
|
|
98
|
+
guard = IsAuthenticated()
|
|
99
|
+
assert guard.guard_name == "is_authenticated"
|
|
100
|
+
assert guard.to_metadata() == {"type": "is_authenticated"}
|
|
101
|
+
|
|
102
|
+
# Should deny without auth context
|
|
103
|
+
assert guard.has_permission(None) == False
|
|
104
|
+
|
|
105
|
+
# Should allow with auth context
|
|
106
|
+
ctx = AuthContext(user_id="user123")
|
|
107
|
+
assert guard.has_permission(ctx) == True
|
|
108
|
+
|
|
109
|
+
def test_is_admin(self):
|
|
110
|
+
"""Test IsAdminUser guard"""
|
|
111
|
+
guard = IsAdminUser()
|
|
112
|
+
assert guard.guard_name == "is_admin"
|
|
113
|
+
|
|
114
|
+
# Should deny without auth
|
|
115
|
+
assert guard.has_permission(None) == False
|
|
116
|
+
|
|
117
|
+
# Should deny non-admin user
|
|
118
|
+
ctx = AuthContext(user_id="user123", is_admin=False)
|
|
119
|
+
assert guard.has_permission(ctx) == False
|
|
120
|
+
|
|
121
|
+
# Should allow admin user
|
|
122
|
+
ctx = AuthContext(user_id="admin", is_admin=True)
|
|
123
|
+
assert guard.has_permission(ctx) == True
|
|
124
|
+
|
|
125
|
+
def test_is_staff(self):
|
|
126
|
+
"""Test IsStaff guard"""
|
|
127
|
+
guard = IsStaff()
|
|
128
|
+
assert guard.guard_name == "is_staff"
|
|
129
|
+
|
|
130
|
+
# Should deny non-staff
|
|
131
|
+
ctx = AuthContext(user_id="user", is_staff=False)
|
|
132
|
+
assert guard.has_permission(ctx) == False
|
|
133
|
+
|
|
134
|
+
# Should allow staff
|
|
135
|
+
ctx = AuthContext(user_id="staff", is_staff=True)
|
|
136
|
+
assert guard.has_permission(ctx) == True
|
|
137
|
+
|
|
138
|
+
def test_has_permission(self):
|
|
139
|
+
"""Test HasPermission guard"""
|
|
140
|
+
guard = HasPermission("users.delete")
|
|
141
|
+
assert guard.guard_name == "has_permission"
|
|
142
|
+
|
|
143
|
+
metadata = guard.to_metadata()
|
|
144
|
+
assert metadata["type"] == "has_permission"
|
|
145
|
+
assert metadata["permission"] == "users.delete"
|
|
146
|
+
|
|
147
|
+
# Should deny without permission
|
|
148
|
+
ctx = AuthContext(user_id="user", permissions={"users.view"})
|
|
149
|
+
assert guard.has_permission(ctx) == False
|
|
150
|
+
|
|
151
|
+
# Should allow with permission
|
|
152
|
+
ctx = AuthContext(user_id="user", permissions={"users.delete", "users.view"})
|
|
153
|
+
assert guard.has_permission(ctx) == True
|
|
154
|
+
|
|
155
|
+
def test_has_any_permission(self):
|
|
156
|
+
"""Test HasAnyPermission guard"""
|
|
157
|
+
guard = HasAnyPermission("users.create", "users.update")
|
|
158
|
+
|
|
159
|
+
metadata = guard.to_metadata()
|
|
160
|
+
assert metadata["type"] == "has_any_permission"
|
|
161
|
+
assert set(metadata["permissions"]) == {"users.create", "users.update"}
|
|
162
|
+
|
|
163
|
+
# Should deny without any permission
|
|
164
|
+
ctx = AuthContext(user_id="user", permissions={"users.view"})
|
|
165
|
+
assert guard.has_permission(ctx) == False
|
|
166
|
+
|
|
167
|
+
# Should allow with one permission
|
|
168
|
+
ctx = AuthContext(user_id="user", permissions={"users.view", "users.create"})
|
|
169
|
+
assert guard.has_permission(ctx) == True
|
|
170
|
+
|
|
171
|
+
def test_has_all_permissions(self):
|
|
172
|
+
"""Test HasAllPermissions guard"""
|
|
173
|
+
guard = HasAllPermissions("users.create", "users.delete")
|
|
174
|
+
|
|
175
|
+
metadata = guard.to_metadata()
|
|
176
|
+
assert metadata["type"] == "has_all_permissions"
|
|
177
|
+
|
|
178
|
+
# Should deny without all permissions
|
|
179
|
+
ctx = AuthContext(user_id="user", permissions={"users.create"})
|
|
180
|
+
assert guard.has_permission(ctx) == False
|
|
181
|
+
|
|
182
|
+
# Should allow with all permissions
|
|
183
|
+
ctx = AuthContext(user_id="user", permissions={"users.create", "users.delete", "users.view"})
|
|
184
|
+
assert guard.has_permission(ctx) == True
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class TestRouteDecoratorAPI:
|
|
188
|
+
"""Test route decorator with guards and auth parameters"""
|
|
189
|
+
|
|
190
|
+
def test_route_with_guards(self):
|
|
191
|
+
"""Test route decorator with guards parameter"""
|
|
192
|
+
api = BoltAPI()
|
|
193
|
+
|
|
194
|
+
@api.get("/admin", guards=[IsAdminUser()])
|
|
195
|
+
async def admin_endpoint():
|
|
196
|
+
return {"message": "admin only"}
|
|
197
|
+
|
|
198
|
+
# Check that metadata was compiled
|
|
199
|
+
handler = api._handlers[0]
|
|
200
|
+
handler_id = 0
|
|
201
|
+
assert handler_id in api._handler_middleware
|
|
202
|
+
|
|
203
|
+
metadata = api._handler_middleware[handler_id]
|
|
204
|
+
assert "guards" in metadata
|
|
205
|
+
assert len(metadata["guards"]) == 1
|
|
206
|
+
assert metadata["guards"][0]["type"] == "is_admin"
|
|
207
|
+
|
|
208
|
+
def test_route_with_auth_override(self):
|
|
209
|
+
"""Test route with custom auth backend"""
|
|
210
|
+
api = BoltAPI()
|
|
211
|
+
|
|
212
|
+
@api.get(
|
|
213
|
+
"/api-only",
|
|
214
|
+
auth=[APIKeyAuthentication(api_keys={"key1"})],
|
|
215
|
+
guards=[IsAuthenticated()]
|
|
216
|
+
)
|
|
217
|
+
async def api_endpoint():
|
|
218
|
+
return {"message": "API key only"}
|
|
219
|
+
|
|
220
|
+
handler_id = 0
|
|
221
|
+
metadata = api._handler_middleware[handler_id]
|
|
222
|
+
|
|
223
|
+
assert "auth_backends" in metadata
|
|
224
|
+
assert len(metadata["auth_backends"]) == 1
|
|
225
|
+
assert metadata["auth_backends"][0]["type"] == "api_key"
|
|
226
|
+
|
|
227
|
+
assert "guards" in metadata
|
|
228
|
+
assert metadata["guards"][0]["type"] == "is_authenticated"
|
|
229
|
+
|
|
230
|
+
def test_route_with_multiple_guards(self):
|
|
231
|
+
"""Test route with multiple guards"""
|
|
232
|
+
api = BoltAPI()
|
|
233
|
+
|
|
234
|
+
@api.get("/restricted", guards=[
|
|
235
|
+
IsAuthenticated(),
|
|
236
|
+
IsStaff(),
|
|
237
|
+
HasPermission("users.delete")
|
|
238
|
+
])
|
|
239
|
+
async def restricted_endpoint():
|
|
240
|
+
return {"message": "highly restricted"}
|
|
241
|
+
|
|
242
|
+
handler_id = 0
|
|
243
|
+
metadata = api._handler_middleware[handler_id]
|
|
244
|
+
|
|
245
|
+
assert len(metadata["guards"]) == 3
|
|
246
|
+
assert metadata["guards"][0]["type"] == "is_authenticated"
|
|
247
|
+
assert metadata["guards"][1]["type"] == "is_staff"
|
|
248
|
+
assert metadata["guards"][2]["type"] == "has_permission"
|
|
249
|
+
|
|
250
|
+
def test_public_route_with_allow_any(self):
|
|
251
|
+
"""Test that AllowAny explicitly bypasses global defaults"""
|
|
252
|
+
api = BoltAPI()
|
|
253
|
+
|
|
254
|
+
@api.get("/public", guards=[AllowAny()])
|
|
255
|
+
async def public_endpoint():
|
|
256
|
+
return {"message": "public"}
|
|
257
|
+
|
|
258
|
+
handler_id = 0
|
|
259
|
+
metadata = api._handler_middleware[handler_id]
|
|
260
|
+
|
|
261
|
+
assert "guards" in metadata
|
|
262
|
+
assert metadata["guards"][0]["type"] == "allow_any"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class TestMetadataCompilation:
|
|
266
|
+
"""Test that metadata is properly compiled and merged"""
|
|
267
|
+
|
|
268
|
+
def test_global_auth_defaults(self):
|
|
269
|
+
"""Test that global auth defaults are used when no per-route auth"""
|
|
270
|
+
import django
|
|
271
|
+
from django.conf import settings
|
|
272
|
+
|
|
273
|
+
if not settings.configured:
|
|
274
|
+
settings.configure(
|
|
275
|
+
SECRET_KEY="test-key",
|
|
276
|
+
BOLT_AUTHENTICATION_CLASSES=[
|
|
277
|
+
JWTAuthentication(secret="global-secret")
|
|
278
|
+
],
|
|
279
|
+
BOLT_DEFAULT_PERMISSION_CLASSES=[IsAuthenticated()]
|
|
280
|
+
)
|
|
281
|
+
django.setup()
|
|
282
|
+
|
|
283
|
+
api = BoltAPI()
|
|
284
|
+
|
|
285
|
+
@api.get("/default-protected")
|
|
286
|
+
async def protected():
|
|
287
|
+
return {"message": "uses global defaults"}
|
|
288
|
+
|
|
289
|
+
# Should use global defaults
|
|
290
|
+
handler_id = 0
|
|
291
|
+
if handler_id in api._handler_middleware:
|
|
292
|
+
metadata = api._handler_middleware[handler_id]
|
|
293
|
+
# Global defaults should be applied
|
|
294
|
+
assert "auth_backends" in metadata or "guards" in metadata
|
|
295
|
+
|
|
296
|
+
def test_per_route_override_precedence(self):
|
|
297
|
+
"""Test that per-route auth/guards override global defaults"""
|
|
298
|
+
api = BoltAPI()
|
|
299
|
+
|
|
300
|
+
@api.get(
|
|
301
|
+
"/override",
|
|
302
|
+
auth=[APIKeyAuthentication(api_keys={"key1"})],
|
|
303
|
+
guards=[AllowAny()]
|
|
304
|
+
)
|
|
305
|
+
async def override_endpoint():
|
|
306
|
+
return {"message": "overrides global"}
|
|
307
|
+
|
|
308
|
+
handler_id = 0
|
|
309
|
+
metadata = api._handler_middleware[handler_id]
|
|
310
|
+
|
|
311
|
+
# Should have per-route config, not global
|
|
312
|
+
assert metadata["auth_backends"][0]["type"] == "api_key"
|
|
313
|
+
assert metadata["guards"][0]["type"] == "allow_any"
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class TestJWTTokenHandling:
|
|
317
|
+
"""Test JWT token creation and validation patterns"""
|
|
318
|
+
|
|
319
|
+
def test_create_valid_jwt(self):
|
|
320
|
+
"""Test creating a valid JWT token"""
|
|
321
|
+
secret = "test-secret"
|
|
322
|
+
payload = {
|
|
323
|
+
"sub": "user123",
|
|
324
|
+
"exp": int(time.time()) + 3600,
|
|
325
|
+
"iat": int(time.time()),
|
|
326
|
+
"is_staff": True,
|
|
327
|
+
"is_admin": False,
|
|
328
|
+
"permissions": ["users.view", "users.create"]
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
token = jwt.encode(payload, secret, algorithm="HS256")
|
|
332
|
+
|
|
333
|
+
# Verify we can decode it
|
|
334
|
+
decoded = jwt.decode(token, secret, algorithms=["HS256"])
|
|
335
|
+
assert decoded["sub"] == "user123"
|
|
336
|
+
assert decoded["is_staff"] == True
|
|
337
|
+
assert "users.view" in decoded["permissions"]
|
|
338
|
+
|
|
339
|
+
def test_expired_jwt(self):
|
|
340
|
+
"""Test that expired tokens are rejected"""
|
|
341
|
+
secret = "test-secret"
|
|
342
|
+
payload = {
|
|
343
|
+
"sub": "user123",
|
|
344
|
+
"exp": int(time.time()) - 100, # Expired 100 seconds ago
|
|
345
|
+
"iat": int(time.time()) - 200,
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
token = jwt.encode(payload, secret, algorithm="HS256")
|
|
349
|
+
|
|
350
|
+
# Should raise ExpiredSignatureError
|
|
351
|
+
with pytest.raises(jwt.ExpiredSignatureError):
|
|
352
|
+
jwt.decode(token, secret, algorithms=["HS256"])
|
|
353
|
+
|
|
354
|
+
def test_invalid_signature(self):
|
|
355
|
+
"""Test that tokens with wrong signature are rejected"""
|
|
356
|
+
token = jwt.encode({"sub": "user"}, "secret1", algorithm="HS256")
|
|
357
|
+
|
|
358
|
+
# Try to decode with different secret
|
|
359
|
+
with pytest.raises(jwt.InvalidSignatureError):
|
|
360
|
+
jwt.decode(token, "secret2", algorithms=["HS256"])
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class TestContextPopulation:
|
|
364
|
+
"""Test that request context is properly populated with auth data"""
|
|
365
|
+
|
|
366
|
+
def test_context_structure(self):
|
|
367
|
+
"""Test expected context structure"""
|
|
368
|
+
# This would be tested in integration tests
|
|
369
|
+
# Here we document the expected structure
|
|
370
|
+
expected_context = {
|
|
371
|
+
"user_id": "user123",
|
|
372
|
+
"is_staff": False,
|
|
373
|
+
"is_admin": False,
|
|
374
|
+
"auth_backend": "jwt",
|
|
375
|
+
"permissions": ["users.view"],
|
|
376
|
+
"auth_claims": {
|
|
377
|
+
"sub": "user123",
|
|
378
|
+
"exp": 1234567890,
|
|
379
|
+
"iat": 1234567800,
|
|
380
|
+
# ... other JWT claims
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
# Verify structure
|
|
385
|
+
assert "user_id" in expected_context
|
|
386
|
+
assert "is_staff" in expected_context
|
|
387
|
+
assert "is_admin" in expected_context
|
|
388
|
+
assert "auth_backend" in expected_context
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class TestEdgeCases:
|
|
392
|
+
"""Test edge cases and error conditions"""
|
|
393
|
+
|
|
394
|
+
def test_empty_guards_list(self):
|
|
395
|
+
"""Test route with empty guards list"""
|
|
396
|
+
api = BoltAPI()
|
|
397
|
+
|
|
398
|
+
@api.get("/no-guards", guards=[])
|
|
399
|
+
async def no_guards():
|
|
400
|
+
return {"message": "no guards"}
|
|
401
|
+
|
|
402
|
+
# Should not create metadata if guards list is empty
|
|
403
|
+
handler_id = 0
|
|
404
|
+
# Empty guards should result in no metadata or default behavior
|
|
405
|
+
|
|
406
|
+
def test_none_auth_parameter(self):
|
|
407
|
+
"""Test route with auth=None (should use global defaults)"""
|
|
408
|
+
api = BoltAPI()
|
|
409
|
+
|
|
410
|
+
@api.get("/default-auth", auth=None, guards=[IsAuthenticated()])
|
|
411
|
+
async def default_auth():
|
|
412
|
+
return {"message": "uses global auth"}
|
|
413
|
+
|
|
414
|
+
# Should fall back to global defaults
|
|
415
|
+
handler_id = 0
|
|
416
|
+
metadata = api._handler_middleware.get(handler_id)
|
|
417
|
+
# Verify it uses global defaults or no auth
|
|
418
|
+
|
|
419
|
+
def test_mixed_guard_types(self):
|
|
420
|
+
"""Test route with mix of instance and class guards"""
|
|
421
|
+
api = BoltAPI()
|
|
422
|
+
|
|
423
|
+
@api.get("/mixed", guards=[IsAuthenticated(), IsStaff])
|
|
424
|
+
async def mixed_guards():
|
|
425
|
+
return {"message": "mixed guards"}
|
|
426
|
+
|
|
427
|
+
handler_id = 0
|
|
428
|
+
metadata = api._handler_middleware[handler_id]
|
|
429
|
+
|
|
430
|
+
# Both should be compiled correctly
|
|
431
|
+
assert len(metadata["guards"]) == 2
|
|
432
|
+
|
|
433
|
+
def test_permission_guard_with_special_chars(self):
|
|
434
|
+
"""Test permission with dots and underscores"""
|
|
435
|
+
guard = HasPermission("myapp.custom_permission.delete")
|
|
436
|
+
metadata = guard.to_metadata()
|
|
437
|
+
assert metadata["permission"] == "myapp.custom_permission.delete"
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
if __name__ == "__main__":
|
|
441
|
+
pytest.main([__file__, "-v"])
|