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,458 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test JWT Token class for Django-Bolt.
|
|
3
|
+
|
|
4
|
+
Tests the Token dataclass for encoding/decoding JWTs with performance focus.
|
|
5
|
+
"""
|
|
6
|
+
import pytest
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
from django_bolt.auth import Token
|
|
9
|
+
import jwt as pyjwt
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestTokenCreation:
|
|
13
|
+
"""Test Token creation and validation"""
|
|
14
|
+
|
|
15
|
+
def test_create_basic_token(self):
|
|
16
|
+
"""Test creating a basic token"""
|
|
17
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
18
|
+
token = Token(
|
|
19
|
+
sub="user123",
|
|
20
|
+
exp=exp,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
assert token.sub == "user123"
|
|
24
|
+
assert token.exp == exp.replace(microsecond=0) # Microseconds stripped
|
|
25
|
+
assert token.iat <= datetime.now(timezone.utc)
|
|
26
|
+
|
|
27
|
+
def test_create_token_with_staff_flags(self):
|
|
28
|
+
"""Test creating token with staff/admin flags"""
|
|
29
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
30
|
+
token = Token(
|
|
31
|
+
sub="admin123",
|
|
32
|
+
exp=exp,
|
|
33
|
+
is_staff=True,
|
|
34
|
+
is_admin=True,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
assert token.is_staff is True
|
|
38
|
+
assert token.is_admin is True
|
|
39
|
+
|
|
40
|
+
def test_create_token_with_permissions(self):
|
|
41
|
+
"""Test creating token with permissions list"""
|
|
42
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
43
|
+
token = Token(
|
|
44
|
+
sub="user123",
|
|
45
|
+
exp=exp,
|
|
46
|
+
permissions=["users.view", "users.create", "posts.edit"],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
assert len(token.permissions) == 3
|
|
50
|
+
assert "users.view" in token.permissions
|
|
51
|
+
|
|
52
|
+
def test_create_token_with_audience_issuer(self):
|
|
53
|
+
"""Test creating token with aud and iss claims"""
|
|
54
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
55
|
+
token = Token(
|
|
56
|
+
sub="user123",
|
|
57
|
+
exp=exp,
|
|
58
|
+
aud="my-api",
|
|
59
|
+
iss="auth-service",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
assert token.aud == "my-api"
|
|
63
|
+
assert token.iss == "auth-service"
|
|
64
|
+
|
|
65
|
+
def test_create_token_with_extras(self):
|
|
66
|
+
"""Test creating token with custom extra claims"""
|
|
67
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
68
|
+
token = Token(
|
|
69
|
+
sub="user123",
|
|
70
|
+
exp=exp,
|
|
71
|
+
extras={
|
|
72
|
+
"tenant_id": "acme-corp",
|
|
73
|
+
"role": "manager",
|
|
74
|
+
"department": "engineering",
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
assert token.extras["tenant_id"] == "acme-corp"
|
|
79
|
+
assert token.extras["role"] == "manager"
|
|
80
|
+
|
|
81
|
+
def test_create_factory_method(self):
|
|
82
|
+
"""Test Token.create() factory method"""
|
|
83
|
+
token = Token.create(
|
|
84
|
+
sub="user123",
|
|
85
|
+
expires_delta=timedelta(minutes=30),
|
|
86
|
+
issuer="my-service",
|
|
87
|
+
is_staff=True,
|
|
88
|
+
permissions=["read", "write"],
|
|
89
|
+
tenant="acme",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
assert token.sub == "user123"
|
|
93
|
+
assert token.iss == "my-service"
|
|
94
|
+
assert token.is_staff is True
|
|
95
|
+
assert token.permissions == ["read", "write"]
|
|
96
|
+
assert token.extras["tenant"] == "acme"
|
|
97
|
+
|
|
98
|
+
# Check expiration is ~30 minutes from now
|
|
99
|
+
now = datetime.now(timezone.utc)
|
|
100
|
+
exp_delta = (token.exp - now).total_seconds()
|
|
101
|
+
assert 1790 < exp_delta < 1810 # ~30 minutes with some tolerance
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TestTokenValidation:
|
|
105
|
+
"""Test Token validation logic"""
|
|
106
|
+
|
|
107
|
+
def test_empty_subject_raises_error(self):
|
|
108
|
+
"""Test that empty subject raises ValueError"""
|
|
109
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
110
|
+
|
|
111
|
+
with pytest.raises(ValueError, match="sub.*must be.*non-empty"):
|
|
112
|
+
Token(sub="", exp=exp)
|
|
113
|
+
|
|
114
|
+
def test_past_expiration_raises_error(self):
|
|
115
|
+
"""Test that past expiration raises ValueError"""
|
|
116
|
+
exp = datetime.now(timezone.utc) - timedelta(hours=1)
|
|
117
|
+
|
|
118
|
+
with pytest.raises(ValueError, match="exp.*must be in the future"):
|
|
119
|
+
Token(sub="user123", exp=exp)
|
|
120
|
+
|
|
121
|
+
def test_future_iat_raises_error(self):
|
|
122
|
+
"""Test that future iat raises ValueError"""
|
|
123
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=2)
|
|
124
|
+
iat = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
125
|
+
|
|
126
|
+
with pytest.raises(ValueError, match="iat.*must be current or past"):
|
|
127
|
+
Token(sub="user123", exp=exp, iat=iat)
|
|
128
|
+
|
|
129
|
+
def test_datetime_normalization(self):
|
|
130
|
+
"""Test that datetimes are normalized to UTC without microseconds"""
|
|
131
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
132
|
+
exp_with_micros = exp.replace(microsecond=123456)
|
|
133
|
+
|
|
134
|
+
token = Token(sub="user123", exp=exp_with_micros)
|
|
135
|
+
|
|
136
|
+
# Should have microseconds stripped
|
|
137
|
+
assert token.exp.microsecond == 0
|
|
138
|
+
assert token.iat.microsecond == 0
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TestTokenEncoding:
|
|
142
|
+
"""Test Token encoding to JWT strings"""
|
|
143
|
+
|
|
144
|
+
def test_encode_basic_token(self):
|
|
145
|
+
"""Test encoding a basic token"""
|
|
146
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
147
|
+
token = Token(sub="user123", exp=exp)
|
|
148
|
+
|
|
149
|
+
secret = "test-secret"
|
|
150
|
+
encoded = token.encode(secret=secret)
|
|
151
|
+
|
|
152
|
+
# Should be a valid JWT string (3 parts separated by dots)
|
|
153
|
+
assert isinstance(encoded, str)
|
|
154
|
+
parts = encoded.split('.')
|
|
155
|
+
assert len(parts) == 3
|
|
156
|
+
|
|
157
|
+
# Decode to verify contents
|
|
158
|
+
decoded = pyjwt.decode(encoded, secret, algorithms=["HS256"])
|
|
159
|
+
assert decoded["sub"] == "user123"
|
|
160
|
+
assert "exp" in decoded
|
|
161
|
+
assert "iat" in decoded
|
|
162
|
+
|
|
163
|
+
def test_encode_with_claims(self):
|
|
164
|
+
"""Test encoding token with all claims"""
|
|
165
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
166
|
+
token = Token(
|
|
167
|
+
sub="user123",
|
|
168
|
+
exp=exp,
|
|
169
|
+
is_staff=True,
|
|
170
|
+
is_admin=False,
|
|
171
|
+
permissions=["read", "write"],
|
|
172
|
+
aud="my-api",
|
|
173
|
+
iss="auth-service",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
secret = "test-secret"
|
|
177
|
+
encoded = token.encode(secret=secret)
|
|
178
|
+
|
|
179
|
+
decoded = pyjwt.decode(
|
|
180
|
+
encoded,
|
|
181
|
+
secret,
|
|
182
|
+
algorithms=["HS256"],
|
|
183
|
+
audience="my-api",
|
|
184
|
+
issuer="auth-service"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
assert decoded["sub"] == "user123"
|
|
188
|
+
assert decoded["is_staff"] is True
|
|
189
|
+
assert decoded["is_admin"] is False
|
|
190
|
+
assert decoded["permissions"] == ["read", "write"]
|
|
191
|
+
assert decoded["aud"] == "my-api"
|
|
192
|
+
assert decoded["iss"] == "auth-service"
|
|
193
|
+
|
|
194
|
+
def test_encode_with_different_algorithms(self):
|
|
195
|
+
"""Test encoding with different algorithms"""
|
|
196
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
197
|
+
token = Token(sub="user123", exp=exp)
|
|
198
|
+
|
|
199
|
+
for algorithm in ["HS256", "HS384", "HS512"]:
|
|
200
|
+
secret = "test-secret-for-" + algorithm
|
|
201
|
+
encoded = token.encode(secret=secret, algorithm=algorithm)
|
|
202
|
+
|
|
203
|
+
decoded = pyjwt.decode(encoded, secret, algorithms=[algorithm])
|
|
204
|
+
assert decoded["sub"] == "user123"
|
|
205
|
+
|
|
206
|
+
def test_encode_with_custom_headers(self):
|
|
207
|
+
"""Test encoding with custom JWT headers"""
|
|
208
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
209
|
+
token = Token(sub="user123", exp=exp)
|
|
210
|
+
|
|
211
|
+
secret = "test-secret"
|
|
212
|
+
headers = {"kid": "key-id-123", "typ": "JWT"}
|
|
213
|
+
encoded = token.encode(secret=secret, headers=headers)
|
|
214
|
+
|
|
215
|
+
# Decode without verification to check headers
|
|
216
|
+
unverified = pyjwt.get_unverified_header(encoded)
|
|
217
|
+
assert unverified["kid"] == "key-id-123"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TestTokenDecoding:
|
|
221
|
+
"""Test Token decoding from JWT strings"""
|
|
222
|
+
|
|
223
|
+
def test_decode_basic_token(self):
|
|
224
|
+
"""Test decoding a basic token"""
|
|
225
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
226
|
+
original = Token(sub="user123", exp=exp)
|
|
227
|
+
|
|
228
|
+
secret = "test-secret"
|
|
229
|
+
encoded = original.encode(secret=secret)
|
|
230
|
+
|
|
231
|
+
# Decode back
|
|
232
|
+
decoded = Token.decode(encoded, secret=secret)
|
|
233
|
+
|
|
234
|
+
assert decoded.sub == "user123"
|
|
235
|
+
assert decoded.exp.timestamp() == pytest.approx(original.exp.timestamp(), abs=1)
|
|
236
|
+
assert decoded.iat.timestamp() == pytest.approx(original.iat.timestamp(), abs=1)
|
|
237
|
+
|
|
238
|
+
def test_decode_with_all_claims(self):
|
|
239
|
+
"""Test decoding token with all claims"""
|
|
240
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
241
|
+
original = Token(
|
|
242
|
+
sub="admin123",
|
|
243
|
+
exp=exp,
|
|
244
|
+
is_staff=True,
|
|
245
|
+
is_admin=True,
|
|
246
|
+
permissions=["full-access"],
|
|
247
|
+
aud="my-api",
|
|
248
|
+
iss="auth-service",
|
|
249
|
+
jti="unique-id-123",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
secret = "test-secret"
|
|
253
|
+
encoded = original.encode(secret=secret)
|
|
254
|
+
|
|
255
|
+
decoded = Token.decode(
|
|
256
|
+
encoded,
|
|
257
|
+
secret=secret,
|
|
258
|
+
audience="my-api",
|
|
259
|
+
issuer="auth-service"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
assert decoded.sub == "admin123"
|
|
263
|
+
assert decoded.is_staff is True
|
|
264
|
+
assert decoded.is_admin is True
|
|
265
|
+
assert decoded.permissions == ["full-access"]
|
|
266
|
+
assert decoded.aud == "my-api"
|
|
267
|
+
assert decoded.iss == "auth-service"
|
|
268
|
+
assert decoded.jti == "unique-id-123"
|
|
269
|
+
|
|
270
|
+
def test_decode_with_extras(self):
|
|
271
|
+
"""Test decoding token with extra claims"""
|
|
272
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
273
|
+
original = Token(
|
|
274
|
+
sub="user123",
|
|
275
|
+
exp=exp,
|
|
276
|
+
extras={"tenant": "acme", "role": "admin", "level": 5}
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
secret = "test-secret"
|
|
280
|
+
encoded = original.encode(secret=secret)
|
|
281
|
+
|
|
282
|
+
decoded = Token.decode(encoded, secret=secret)
|
|
283
|
+
|
|
284
|
+
assert decoded.extras["tenant"] == "acme"
|
|
285
|
+
assert decoded.extras["role"] == "admin"
|
|
286
|
+
assert decoded.extras["level"] == 5
|
|
287
|
+
|
|
288
|
+
def test_decode_expired_token_raises_error(self):
|
|
289
|
+
"""Test that decoding expired token raises ValueError"""
|
|
290
|
+
# Create token that will be expired
|
|
291
|
+
payload = {
|
|
292
|
+
"sub": "user123",
|
|
293
|
+
"exp": int((datetime.now(timezone.utc) - timedelta(hours=1)).timestamp()),
|
|
294
|
+
"iat": int((datetime.now(timezone.utc) - timedelta(hours=2)).timestamp()),
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
secret = "test-secret"
|
|
298
|
+
encoded = pyjwt.encode(payload, secret, algorithm="HS256")
|
|
299
|
+
|
|
300
|
+
with pytest.raises(ValueError, match="Failed to decode token"):
|
|
301
|
+
Token.decode(encoded, secret=secret)
|
|
302
|
+
|
|
303
|
+
def test_decode_with_wrong_secret_raises_error(self):
|
|
304
|
+
"""Test that decoding with wrong secret raises ValueError"""
|
|
305
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
306
|
+
token = Token(sub="user123", exp=exp)
|
|
307
|
+
|
|
308
|
+
encoded = token.encode(secret="secret1")
|
|
309
|
+
|
|
310
|
+
with pytest.raises(ValueError, match="Failed to decode token"):
|
|
311
|
+
Token.decode(encoded, secret="wrong-secret")
|
|
312
|
+
|
|
313
|
+
def test_decode_with_audience_mismatch_raises_error(self):
|
|
314
|
+
"""Test that audience mismatch raises ValueError"""
|
|
315
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
316
|
+
token = Token(sub="user123", exp=exp, aud="api1")
|
|
317
|
+
|
|
318
|
+
secret = "test-secret"
|
|
319
|
+
encoded = token.encode(secret=secret)
|
|
320
|
+
|
|
321
|
+
with pytest.raises(ValueError, match="Failed to decode token"):
|
|
322
|
+
Token.decode(encoded, secret=secret, audience="wrong-audience")
|
|
323
|
+
|
|
324
|
+
def test_decode_skip_expiry_verification(self):
|
|
325
|
+
"""Test decoding with expiry verification disabled"""
|
|
326
|
+
# Create expired token
|
|
327
|
+
payload = {
|
|
328
|
+
"sub": "user123",
|
|
329
|
+
"exp": int((datetime.now(timezone.utc) - timedelta(hours=1)).timestamp()),
|
|
330
|
+
"iat": int((datetime.now(timezone.utc) - timedelta(hours=2)).timestamp()),
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
secret = "test-secret"
|
|
334
|
+
encoded = pyjwt.encode(payload, secret, algorithm="HS256")
|
|
335
|
+
|
|
336
|
+
# Should succeed with verify_exp=False
|
|
337
|
+
decoded = Token.decode(encoded, secret=secret, verify_exp=False)
|
|
338
|
+
assert decoded.sub == "user123"
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class TestTokenToDictConversion:
|
|
342
|
+
"""Test Token.to_dict() conversion"""
|
|
343
|
+
|
|
344
|
+
def test_to_dict_basic(self):
|
|
345
|
+
"""Test converting token to dict"""
|
|
346
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
347
|
+
token = Token(sub="user123", exp=exp)
|
|
348
|
+
|
|
349
|
+
d = token.to_dict()
|
|
350
|
+
|
|
351
|
+
assert d["sub"] == "user123"
|
|
352
|
+
assert isinstance(d["exp"], int) # Unix timestamp
|
|
353
|
+
assert isinstance(d["iat"], int)
|
|
354
|
+
assert d["exp"] > d["iat"]
|
|
355
|
+
|
|
356
|
+
def test_to_dict_with_optional_fields(self):
|
|
357
|
+
"""Test to_dict with optional fields"""
|
|
358
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
359
|
+
token = Token(
|
|
360
|
+
sub="user123",
|
|
361
|
+
exp=exp,
|
|
362
|
+
aud="my-api",
|
|
363
|
+
iss="auth",
|
|
364
|
+
jti="unique",
|
|
365
|
+
is_staff=True,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
d = token.to_dict()
|
|
369
|
+
|
|
370
|
+
assert d["aud"] == "my-api"
|
|
371
|
+
assert d["iss"] == "auth"
|
|
372
|
+
assert d["jti"] == "unique"
|
|
373
|
+
assert d["is_staff"] is True
|
|
374
|
+
|
|
375
|
+
def test_to_dict_excludes_none_values(self):
|
|
376
|
+
"""Test that None values are excluded from dict"""
|
|
377
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
378
|
+
token = Token(
|
|
379
|
+
sub="user123",
|
|
380
|
+
exp=exp,
|
|
381
|
+
aud=None,
|
|
382
|
+
iss=None,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
d = token.to_dict()
|
|
386
|
+
|
|
387
|
+
assert "aud" not in d
|
|
388
|
+
assert "iss" not in d
|
|
389
|
+
|
|
390
|
+
def test_to_dict_includes_extras(self):
|
|
391
|
+
"""Test that extras are included in dict"""
|
|
392
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
393
|
+
token = Token(
|
|
394
|
+
sub="user123",
|
|
395
|
+
exp=exp,
|
|
396
|
+
extras={"custom1": "value1", "custom2": 42}
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
d = token.to_dict()
|
|
400
|
+
|
|
401
|
+
assert d["custom1"] == "value1"
|
|
402
|
+
assert d["custom2"] == 42
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class TestTokenRoundTrip:
|
|
406
|
+
"""Test encoding and decoding round-trip"""
|
|
407
|
+
|
|
408
|
+
def test_round_trip_preserves_data(self):
|
|
409
|
+
"""Test that encode->decode preserves all data"""
|
|
410
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
411
|
+
original = Token(
|
|
412
|
+
sub="user123",
|
|
413
|
+
exp=exp,
|
|
414
|
+
is_staff=True,
|
|
415
|
+
is_admin=False,
|
|
416
|
+
permissions=["read", "write", "delete"],
|
|
417
|
+
aud="my-api",
|
|
418
|
+
iss="auth-service",
|
|
419
|
+
extras={"tenant": "acme", "role": "manager"}
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
secret = "test-secret"
|
|
423
|
+
encoded = original.encode(secret=secret)
|
|
424
|
+
decoded = Token.decode(
|
|
425
|
+
encoded,
|
|
426
|
+
secret=secret,
|
|
427
|
+
audience="my-api",
|
|
428
|
+
issuer="auth-service"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Compare all fields (timestamps may differ slightly due to rounding)
|
|
432
|
+
assert decoded.sub == original.sub
|
|
433
|
+
assert decoded.exp.timestamp() == pytest.approx(original.exp.timestamp(), abs=1)
|
|
434
|
+
assert decoded.is_staff == original.is_staff
|
|
435
|
+
assert decoded.is_admin == original.is_admin
|
|
436
|
+
assert decoded.permissions == original.permissions
|
|
437
|
+
assert decoded.aud == original.aud
|
|
438
|
+
assert decoded.iss == original.iss
|
|
439
|
+
assert decoded.extras == original.extras
|
|
440
|
+
|
|
441
|
+
def test_multiple_round_trips(self):
|
|
442
|
+
"""Test multiple encode/decode cycles"""
|
|
443
|
+
exp = datetime.now(timezone.utc) + timedelta(hours=1)
|
|
444
|
+
token = Token(sub="user123", exp=exp, is_staff=True)
|
|
445
|
+
|
|
446
|
+
secret = "test-secret"
|
|
447
|
+
|
|
448
|
+
# Multiple round trips
|
|
449
|
+
for _ in range(3):
|
|
450
|
+
encoded = token.encode(secret=secret)
|
|
451
|
+
token = Token.decode(encoded, secret=secret)
|
|
452
|
+
|
|
453
|
+
assert token.sub == "user123"
|
|
454
|
+
assert token.is_staff is True
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
if __name__ == "__main__":
|
|
458
|
+
pytest.main([__file__, "-v"])
|