django-bolt 0.1.0__cp310-abi3-win_amd64.whl → 0.1.1__cp310-abi3-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-bolt might be problematic. Click here for more details.
- django_bolt/__init__.py +2 -2
- django_bolt/_core.pyd +0 -0
- django_bolt/_json.py +169 -0
- django_bolt/admin/static_routes.py +15 -21
- django_bolt/api.py +181 -61
- django_bolt/auth/__init__.py +2 -2
- django_bolt/decorators.py +15 -3
- django_bolt/dependencies.py +30 -24
- django_bolt/error_handlers.py +2 -1
- django_bolt/openapi/plugins.py +3 -2
- django_bolt/openapi/schema_generator.py +65 -20
- django_bolt/pagination.py +2 -1
- django_bolt/responses.py +3 -2
- django_bolt/serialization.py +5 -4
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.1.dist-info}/METADATA +179 -197
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.1.dist-info}/RECORD +18 -55
- django_bolt/auth/README.md +0 -464
- django_bolt/auth/REVOCATION_EXAMPLE.md +0 -391
- django_bolt/tests/__init__.py +0 -0
- django_bolt/tests/admin_tests/__init__.py +0 -1
- django_bolt/tests/admin_tests/conftest.py +0 -6
- django_bolt/tests/admin_tests/test_admin_with_django.py +0 -278
- django_bolt/tests/admin_tests/urls.py +0 -9
- django_bolt/tests/cbv/__init__.py +0 -0
- django_bolt/tests/cbv/test_class_views.py +0 -570
- django_bolt/tests/cbv/test_class_views_django_orm.py +0 -703
- django_bolt/tests/cbv/test_class_views_features.py +0 -1173
- django_bolt/tests/cbv/test_class_views_with_client.py +0 -622
- django_bolt/tests/conftest.py +0 -165
- django_bolt/tests/test_action_decorator.py +0 -399
- django_bolt/tests/test_auth_secret_key.py +0 -83
- django_bolt/tests/test_decorator_syntax.py +0 -159
- django_bolt/tests/test_error_handling.py +0 -481
- django_bolt/tests/test_file_response.py +0 -192
- django_bolt/tests/test_global_cors.py +0 -172
- django_bolt/tests/test_guards_auth.py +0 -441
- django_bolt/tests/test_guards_integration.py +0 -303
- django_bolt/tests/test_health.py +0 -283
- django_bolt/tests/test_integration_validation.py +0 -400
- django_bolt/tests/test_json_validation.py +0 -536
- django_bolt/tests/test_jwt_auth.py +0 -327
- django_bolt/tests/test_jwt_token.py +0 -458
- django_bolt/tests/test_logging.py +0 -837
- django_bolt/tests/test_logging_merge.py +0 -419
- django_bolt/tests/test_middleware.py +0 -492
- django_bolt/tests/test_middleware_server.py +0 -230
- django_bolt/tests/test_model_viewset.py +0 -323
- django_bolt/tests/test_models.py +0 -24
- django_bolt/tests/test_pagination.py +0 -1258
- django_bolt/tests/test_parameter_validation.py +0 -178
- django_bolt/tests/test_syntax.py +0 -626
- django_bolt/tests/test_testing_utilities.py +0 -163
- django_bolt/tests/test_testing_utilities_simple.py +0 -123
- django_bolt/tests/test_viewset_unified.py +0 -346
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.1.dist-info}/WHEEL +0 -0
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Integration tests for guards and authentication with TestClient.
|
|
3
|
-
|
|
4
|
-
Tests the full request flow including Rust-side authentication and guard evaluation.
|
|
5
|
-
"""
|
|
6
|
-
import jwt
|
|
7
|
-
import time
|
|
8
|
-
import pytest
|
|
9
|
-
from django_bolt import BoltAPI
|
|
10
|
-
from django_bolt.auth import JWTAuthentication, APIKeyAuthentication
|
|
11
|
-
from django_bolt.auth import (
|
|
12
|
-
AllowAny, IsAuthenticated, IsAdminUser, IsStaff,
|
|
13
|
-
HasPermission, HasAnyPermission
|
|
14
|
-
)
|
|
15
|
-
from django_bolt.testing import TestClient
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def create_token(user_id="user123", is_staff=False, is_admin=False, permissions=None):
|
|
19
|
-
"""Helper to create JWT tokens"""
|
|
20
|
-
payload = {
|
|
21
|
-
"sub": user_id,
|
|
22
|
-
"exp": int(time.time()) + 3600,
|
|
23
|
-
"iat": int(time.time()),
|
|
24
|
-
"is_staff": is_staff,
|
|
25
|
-
"is_superuser": is_admin,
|
|
26
|
-
"permissions": permissions or []
|
|
27
|
-
}
|
|
28
|
-
return jwt.encode(payload, "test-secret", algorithm="HS256")
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@pytest.fixture(scope="module")
|
|
32
|
-
def api():
|
|
33
|
-
"""Create test API with guards and authentication"""
|
|
34
|
-
# Setup Django
|
|
35
|
-
import django
|
|
36
|
-
from django.conf import settings
|
|
37
|
-
from django.core.management import call_command
|
|
38
|
-
|
|
39
|
-
if not settings.configured:
|
|
40
|
-
settings.configure(
|
|
41
|
-
DEBUG=True,
|
|
42
|
-
SECRET_KEY='test-secret-key-for-guards',
|
|
43
|
-
INSTALLED_APPS=[
|
|
44
|
-
'django.contrib.contenttypes',
|
|
45
|
-
'django.contrib.auth',
|
|
46
|
-
'django_bolt',
|
|
47
|
-
],
|
|
48
|
-
DATABASES={
|
|
49
|
-
'default': {
|
|
50
|
-
'ENGINE': 'django.db.backends.sqlite3',
|
|
51
|
-
'NAME': ':memory:',
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
USE_TZ=True,
|
|
55
|
-
)
|
|
56
|
-
django.setup()
|
|
57
|
-
# Run migrations to create tables
|
|
58
|
-
call_command('migrate', '--run-syncdb', verbosity=0)
|
|
59
|
-
|
|
60
|
-
api = BoltAPI()
|
|
61
|
-
|
|
62
|
-
# Public endpoint with AllowAny
|
|
63
|
-
@api.get("/public", guards=[AllowAny()])
|
|
64
|
-
async def public_endpoint():
|
|
65
|
-
return {"message": "public", "auth": "not required"}
|
|
66
|
-
|
|
67
|
-
# Protected endpoint requiring authentication
|
|
68
|
-
@api.get(
|
|
69
|
-
"/protected",
|
|
70
|
-
auth=[JWTAuthentication(secret="test-secret")],
|
|
71
|
-
guards=[IsAuthenticated()]
|
|
72
|
-
)
|
|
73
|
-
async def protected_endpoint(request: dict):
|
|
74
|
-
context = request.get("context", {})
|
|
75
|
-
return {
|
|
76
|
-
"message": "protected",
|
|
77
|
-
"user_id": context.get("user_id"),
|
|
78
|
-
"is_staff": context.get("is_staff", False),
|
|
79
|
-
"is_admin": context.get("is_admin", False),
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
# Admin-only endpoint
|
|
83
|
-
@api.get(
|
|
84
|
-
"/admin",
|
|
85
|
-
auth=[JWTAuthentication(secret="test-secret")],
|
|
86
|
-
guards=[IsAdminUser()]
|
|
87
|
-
)
|
|
88
|
-
async def admin_endpoint(request: dict):
|
|
89
|
-
context = request["context"]
|
|
90
|
-
return {
|
|
91
|
-
"message": "admin area",
|
|
92
|
-
"user_id": context["user_id"],
|
|
93
|
-
"is_admin": context["is_admin"],
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
# Staff-only endpoint
|
|
97
|
-
@api.get(
|
|
98
|
-
"/staff",
|
|
99
|
-
auth=[JWTAuthentication(secret="test-secret")],
|
|
100
|
-
guards=[IsStaff()]
|
|
101
|
-
)
|
|
102
|
-
async def staff_endpoint(request: dict):
|
|
103
|
-
return {
|
|
104
|
-
"message": "staff area",
|
|
105
|
-
"user_id": request["context"]["user_id"],
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
# Permission-required endpoint
|
|
109
|
-
@api.get(
|
|
110
|
-
"/delete-users",
|
|
111
|
-
auth=[JWTAuthentication(secret="test-secret")],
|
|
112
|
-
guards=[HasPermission("users.delete")]
|
|
113
|
-
)
|
|
114
|
-
async def delete_users_endpoint():
|
|
115
|
-
return {"message": "deleting users"}
|
|
116
|
-
|
|
117
|
-
# Multiple permissions (any)
|
|
118
|
-
@api.get(
|
|
119
|
-
"/moderate",
|
|
120
|
-
auth=[JWTAuthentication(secret="test-secret")],
|
|
121
|
-
guards=[HasAnyPermission("users.moderate", "posts.moderate")]
|
|
122
|
-
)
|
|
123
|
-
async def moderate_endpoint():
|
|
124
|
-
return {"message": "moderating content"}
|
|
125
|
-
|
|
126
|
-
# API key authentication
|
|
127
|
-
@api.get(
|
|
128
|
-
"/api-endpoint",
|
|
129
|
-
auth=[APIKeyAuthentication(api_keys={"valid-key-123", "valid-key-456"})],
|
|
130
|
-
guards=[IsAuthenticated()]
|
|
131
|
-
)
|
|
132
|
-
async def api_key_endpoint(request: dict):
|
|
133
|
-
return {
|
|
134
|
-
"message": "API key valid",
|
|
135
|
-
"user_id": request["context"].get("user_id"),
|
|
136
|
-
"backend": request["context"].get("auth_backend"),
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
# Full context inspection endpoint
|
|
140
|
-
@api.get(
|
|
141
|
-
"/context",
|
|
142
|
-
auth=[JWTAuthentication(secret="test-secret")],
|
|
143
|
-
guards=[IsAuthenticated()]
|
|
144
|
-
)
|
|
145
|
-
async def context_endpoint(request: dict):
|
|
146
|
-
context = request.get("context", {})
|
|
147
|
-
return {
|
|
148
|
-
"context_keys": list(context.keys()) if hasattr(context, 'keys') else [],
|
|
149
|
-
"user_id": context.get("user_id"),
|
|
150
|
-
"is_staff": context.get("is_staff"),
|
|
151
|
-
"is_admin": context.get("is_admin"),
|
|
152
|
-
"auth_backend": context.get("auth_backend"),
|
|
153
|
-
"has_claims": "auth_claims" in context,
|
|
154
|
-
"has_permissions": "permissions" in context,
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return api
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
@pytest.fixture(scope="module")
|
|
161
|
-
def client(api):
|
|
162
|
-
"""Create TestClient for the API"""
|
|
163
|
-
with TestClient(api) as client:
|
|
164
|
-
yield client
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def test_public_endpoint(client):
|
|
168
|
-
"""Test public endpoint (AllowAny)"""
|
|
169
|
-
response = client.get("/public")
|
|
170
|
-
assert response.status_code == 200
|
|
171
|
-
assert response.json()["auth"] == "not required"
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def test_protected_endpoint_without_token(client):
|
|
175
|
-
"""Test protected endpoint without token (should fail with 401)"""
|
|
176
|
-
response = client.get("/protected")
|
|
177
|
-
assert response.status_code == 401
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def test_protected_endpoint_with_valid_token(client):
|
|
181
|
-
"""Test protected endpoint with valid token"""
|
|
182
|
-
token = create_token(user_id="user123")
|
|
183
|
-
response = client.get("/protected", headers={"Authorization": f"Bearer {token}"})
|
|
184
|
-
assert response.status_code == 200
|
|
185
|
-
data = response.json()
|
|
186
|
-
assert data["message"] == "protected"
|
|
187
|
-
assert data["user_id"] == "user123"
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def test_admin_endpoint_with_non_admin_token(client):
|
|
191
|
-
"""Test admin endpoint with non-admin token (should fail with 403)"""
|
|
192
|
-
token = create_token(user_id="regular-user", is_admin=False)
|
|
193
|
-
response = client.get("/admin", headers={"Authorization": f"Bearer {token}"})
|
|
194
|
-
assert response.status_code == 403
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def test_admin_endpoint_with_admin_token(client):
|
|
198
|
-
"""Test admin endpoint with admin token"""
|
|
199
|
-
token = create_token(user_id="admin-user", is_admin=True)
|
|
200
|
-
response = client.get("/admin", headers={"Authorization": f"Bearer {token}"})
|
|
201
|
-
assert response.status_code == 200
|
|
202
|
-
data = response.json()
|
|
203
|
-
assert data["message"] == "admin area"
|
|
204
|
-
assert data["is_admin"] is True
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def test_staff_endpoint_with_non_staff_token(client):
|
|
208
|
-
"""Test staff endpoint with non-staff token"""
|
|
209
|
-
token = create_token(user_id="regular-user", is_staff=False)
|
|
210
|
-
response = client.get("/staff", headers={"Authorization": f"Bearer {token}"})
|
|
211
|
-
assert response.status_code == 403
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def test_staff_endpoint_with_staff_token(client):
|
|
215
|
-
"""Test staff endpoint with staff token"""
|
|
216
|
-
token = create_token(user_id="staff-user", is_staff=True)
|
|
217
|
-
response = client.get("/staff", headers={"Authorization": f"Bearer {token}"})
|
|
218
|
-
assert response.status_code == 200
|
|
219
|
-
data = response.json()
|
|
220
|
-
assert data["message"] == "staff area"
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def test_permission_based_endpoint_without_permission(client):
|
|
224
|
-
"""Test permission-based endpoint without required permission"""
|
|
225
|
-
token = create_token(user_id="user-no-perms", permissions=[])
|
|
226
|
-
response = client.get("/delete-users", headers={"Authorization": f"Bearer {token}"})
|
|
227
|
-
assert response.status_code == 403
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def test_permission_based_endpoint_with_permission(client):
|
|
231
|
-
"""Test permission-based endpoint with required permission"""
|
|
232
|
-
token = create_token(user_id="user-with-perms", permissions=["users.delete"])
|
|
233
|
-
response = client.get("/delete-users", headers={"Authorization": f"Bearer {token}"})
|
|
234
|
-
assert response.status_code == 200
|
|
235
|
-
assert response.json()["message"] == "deleting users"
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def test_has_any_permission_with_one_match(client):
|
|
239
|
-
"""Test HasAnyPermission guard with one matching permission"""
|
|
240
|
-
token = create_token(user_id="moderator", permissions=["users.moderate"])
|
|
241
|
-
response = client.get("/moderate", headers={"Authorization": f"Bearer {token}"})
|
|
242
|
-
assert response.status_code == 200
|
|
243
|
-
assert response.json()["message"] == "moderating content"
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def test_has_any_permission_without_match(client):
|
|
247
|
-
"""Test HasAnyPermission guard without any matching permission"""
|
|
248
|
-
token = create_token(user_id="user", permissions=["other.permission"])
|
|
249
|
-
response = client.get("/moderate", headers={"Authorization": f"Bearer {token}"})
|
|
250
|
-
assert response.status_code == 403
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
def test_api_key_authentication_without_key(client):
|
|
254
|
-
"""Test API key authentication without key"""
|
|
255
|
-
response = client.get("/api-endpoint")
|
|
256
|
-
assert response.status_code == 401
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def test_api_key_authentication_with_valid_key(client):
|
|
260
|
-
"""Test API key authentication with valid key"""
|
|
261
|
-
response = client.get("/api-endpoint", headers={"X-API-Key": "valid-key-123"})
|
|
262
|
-
assert response.status_code == 200
|
|
263
|
-
data = response.json()
|
|
264
|
-
assert data["message"] == "API key valid"
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def test_api_key_authentication_with_invalid_key(client):
|
|
268
|
-
"""Test API key authentication with invalid key"""
|
|
269
|
-
response = client.get("/api-endpoint", headers={"X-API-Key": "invalid-key"})
|
|
270
|
-
assert response.status_code == 401
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
def test_context_population(client):
|
|
274
|
-
"""Test that context is properly populated"""
|
|
275
|
-
token = create_token(user_id="context-user", is_staff=True, permissions=["test.permission"])
|
|
276
|
-
response = client.get("/context", headers={"Authorization": f"Bearer {token}"})
|
|
277
|
-
assert response.status_code == 200
|
|
278
|
-
data = response.json()
|
|
279
|
-
assert data["user_id"] == "context-user"
|
|
280
|
-
assert data["is_staff"] is True
|
|
281
|
-
assert "context_keys" in data
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
def test_invalid_jwt_signature(client):
|
|
285
|
-
"""Test invalid JWT signature"""
|
|
286
|
-
token = jwt.encode(
|
|
287
|
-
{"sub": "user123", "exp": int(time.time()) + 3600},
|
|
288
|
-
"wrong-secret", # Different secret
|
|
289
|
-
algorithm="HS256"
|
|
290
|
-
)
|
|
291
|
-
response = client.get("/protected", headers={"Authorization": f"Bearer {token}"})
|
|
292
|
-
assert response.status_code == 401
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
def test_expired_jwt_token(client):
|
|
296
|
-
"""Test expired JWT token"""
|
|
297
|
-
token = jwt.encode(
|
|
298
|
-
{"sub": "user123", "exp": int(time.time()) - 3600}, # Expired
|
|
299
|
-
"test-secret",
|
|
300
|
-
algorithm="HS256"
|
|
301
|
-
)
|
|
302
|
-
response = client.get("/protected", headers={"Authorization": f"Bearer {token}"})
|
|
303
|
-
assert response.status_code == 401
|
django_bolt/tests/test_health.py
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
"""Tests for Django-Bolt health check system."""
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
import asyncio
|
|
5
|
-
from django_bolt.health import (
|
|
6
|
-
HealthCheck,
|
|
7
|
-
check_database,
|
|
8
|
-
health_handler,
|
|
9
|
-
ready_handler,
|
|
10
|
-
add_health_check,
|
|
11
|
-
register_health_checks,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class TestHealthCheck:
|
|
16
|
-
"""Test HealthCheck class."""
|
|
17
|
-
|
|
18
|
-
def test_health_check_initialization(self):
|
|
19
|
-
"""Test HealthCheck initialization."""
|
|
20
|
-
hc = HealthCheck()
|
|
21
|
-
assert hc._checks == []
|
|
22
|
-
|
|
23
|
-
def test_add_check(self):
|
|
24
|
-
"""Test adding custom health checks."""
|
|
25
|
-
hc = HealthCheck()
|
|
26
|
-
|
|
27
|
-
async def custom_check():
|
|
28
|
-
return True, "OK"
|
|
29
|
-
|
|
30
|
-
hc.add_check(custom_check)
|
|
31
|
-
assert len(hc._checks) == 1
|
|
32
|
-
assert hc._checks[0] == custom_check
|
|
33
|
-
|
|
34
|
-
@pytest.mark.asyncio
|
|
35
|
-
async def test_run_checks_all_healthy(self):
|
|
36
|
-
"""Test run_checks when all checks pass."""
|
|
37
|
-
hc = HealthCheck()
|
|
38
|
-
|
|
39
|
-
async def check1():
|
|
40
|
-
return True, "Check 1 OK"
|
|
41
|
-
|
|
42
|
-
async def check2():
|
|
43
|
-
return True, "Check 2 OK"
|
|
44
|
-
|
|
45
|
-
hc.add_check(check1)
|
|
46
|
-
hc.add_check(check2)
|
|
47
|
-
|
|
48
|
-
results = await hc.run_checks()
|
|
49
|
-
assert results["status"] == "healthy"
|
|
50
|
-
assert "check1" in results["checks"]
|
|
51
|
-
assert "check2" in results["checks"]
|
|
52
|
-
assert results["checks"]["check1"]["healthy"] is True
|
|
53
|
-
assert results["checks"]["check2"]["healthy"] is True
|
|
54
|
-
|
|
55
|
-
@pytest.mark.asyncio
|
|
56
|
-
async def test_run_checks_one_unhealthy(self):
|
|
57
|
-
"""Test run_checks when one check fails."""
|
|
58
|
-
hc = HealthCheck()
|
|
59
|
-
|
|
60
|
-
async def check1():
|
|
61
|
-
return True, "Check 1 OK"
|
|
62
|
-
|
|
63
|
-
async def check2():
|
|
64
|
-
return False, "Check 2 failed"
|
|
65
|
-
|
|
66
|
-
hc.add_check(check1)
|
|
67
|
-
hc.add_check(check2)
|
|
68
|
-
|
|
69
|
-
results = await hc.run_checks()
|
|
70
|
-
assert results["status"] == "unhealthy"
|
|
71
|
-
assert results["checks"]["check1"]["healthy"] is True
|
|
72
|
-
assert results["checks"]["check2"]["healthy"] is False
|
|
73
|
-
assert "failed" in results["checks"]["check2"]["message"]
|
|
74
|
-
|
|
75
|
-
@pytest.mark.asyncio
|
|
76
|
-
async def test_run_checks_exception_handling(self):
|
|
77
|
-
"""Test run_checks handles exceptions in checks."""
|
|
78
|
-
hc = HealthCheck()
|
|
79
|
-
|
|
80
|
-
async def failing_check():
|
|
81
|
-
raise RuntimeError("Check crashed")
|
|
82
|
-
|
|
83
|
-
hc.add_check(failing_check)
|
|
84
|
-
|
|
85
|
-
results = await hc.run_checks()
|
|
86
|
-
assert results["status"] == "unhealthy"
|
|
87
|
-
assert results["checks"]["failing_check"]["healthy"] is False
|
|
88
|
-
assert "Check crashed" in results["checks"]["failing_check"]["message"]
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
class TestDatabaseCheck:
|
|
92
|
-
"""Test database health check."""
|
|
93
|
-
|
|
94
|
-
@pytest.mark.asyncio
|
|
95
|
-
async def test_check_database_success(self):
|
|
96
|
-
"""Test database check succeeds with valid connection."""
|
|
97
|
-
# This test requires Django to be configured
|
|
98
|
-
try:
|
|
99
|
-
from django.conf import settings
|
|
100
|
-
if not settings.configured:
|
|
101
|
-
pytest.skip("Django not configured")
|
|
102
|
-
|
|
103
|
-
healthy, message = await check_database()
|
|
104
|
-
# Should either succeed or fail gracefully
|
|
105
|
-
assert isinstance(healthy, bool)
|
|
106
|
-
assert isinstance(message, str)
|
|
107
|
-
except ImportError:
|
|
108
|
-
pytest.skip("Django not available")
|
|
109
|
-
|
|
110
|
-
@pytest.mark.asyncio
|
|
111
|
-
async def test_check_database_handles_error(self):
|
|
112
|
-
"""Test database check handles connection errors."""
|
|
113
|
-
# Even if database is not available, check should not raise
|
|
114
|
-
try:
|
|
115
|
-
healthy, message = await check_database()
|
|
116
|
-
assert isinstance(healthy, bool)
|
|
117
|
-
assert isinstance(message, str)
|
|
118
|
-
except Exception:
|
|
119
|
-
pytest.fail("check_database should not raise exceptions")
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
class TestHealthHandlers:
|
|
123
|
-
"""Test health endpoint handlers."""
|
|
124
|
-
|
|
125
|
-
@pytest.mark.asyncio
|
|
126
|
-
async def test_health_handler(self):
|
|
127
|
-
"""Test basic health endpoint."""
|
|
128
|
-
result = await health_handler()
|
|
129
|
-
assert isinstance(result, dict)
|
|
130
|
-
assert result["status"] == "ok"
|
|
131
|
-
|
|
132
|
-
@pytest.mark.asyncio
|
|
133
|
-
async def test_ready_handler(self):
|
|
134
|
-
"""Test readiness endpoint."""
|
|
135
|
-
result = await ready_handler()
|
|
136
|
-
assert isinstance(result, dict)
|
|
137
|
-
assert "status" in result
|
|
138
|
-
assert "checks" in result
|
|
139
|
-
# Status should be healthy or unhealthy
|
|
140
|
-
assert result["status"] in ["healthy", "unhealthy"]
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
class TestHealthIntegration:
|
|
144
|
-
"""Integration tests for health checks."""
|
|
145
|
-
|
|
146
|
-
def test_register_health_checks(self):
|
|
147
|
-
"""Test registering health checks on API."""
|
|
148
|
-
from django_bolt import BoltAPI
|
|
149
|
-
|
|
150
|
-
api = BoltAPI()
|
|
151
|
-
initial_route_count = len(api._routes)
|
|
152
|
-
|
|
153
|
-
register_health_checks(api)
|
|
154
|
-
|
|
155
|
-
# Check that routes were registered (should have 2 more routes)
|
|
156
|
-
assert len(api._routes) == initial_route_count + 2
|
|
157
|
-
|
|
158
|
-
# Check handler names contain health/ready
|
|
159
|
-
handlers = [api._handlers[route[2]] for route in api._routes[initial_route_count:]]
|
|
160
|
-
handler_names = [h.__name__ for h in handlers]
|
|
161
|
-
assert "health_handler" in handler_names
|
|
162
|
-
assert "ready_handler" in handler_names
|
|
163
|
-
|
|
164
|
-
def test_add_health_check_global(self):
|
|
165
|
-
"""Test adding global health check."""
|
|
166
|
-
async def custom_check():
|
|
167
|
-
return True, "Custom check OK"
|
|
168
|
-
|
|
169
|
-
add_health_check(custom_check)
|
|
170
|
-
|
|
171
|
-
# Check should be added to global instance
|
|
172
|
-
from django_bolt.health import _health_check
|
|
173
|
-
assert custom_check in _health_check._checks
|
|
174
|
-
|
|
175
|
-
# Clean up
|
|
176
|
-
_health_check._checks.remove(custom_check)
|
|
177
|
-
|
|
178
|
-
@pytest.mark.asyncio
|
|
179
|
-
async def test_custom_health_check_integration(self):
|
|
180
|
-
"""Test custom health check integration."""
|
|
181
|
-
from django_bolt.health import _health_check
|
|
182
|
-
|
|
183
|
-
# Clear existing checks
|
|
184
|
-
original_checks = _health_check._checks.copy()
|
|
185
|
-
_health_check._checks.clear()
|
|
186
|
-
|
|
187
|
-
# Add custom check
|
|
188
|
-
async def redis_check():
|
|
189
|
-
# Simulate Redis check
|
|
190
|
-
return True, "Redis OK"
|
|
191
|
-
|
|
192
|
-
add_health_check(redis_check)
|
|
193
|
-
|
|
194
|
-
# Run ready handler
|
|
195
|
-
result = await ready_handler()
|
|
196
|
-
assert result["status"] == "healthy"
|
|
197
|
-
assert "redis_check" in result["checks"]
|
|
198
|
-
assert result["checks"]["redis_check"]["healthy"] is True
|
|
199
|
-
|
|
200
|
-
# Restore original checks
|
|
201
|
-
_health_check._checks = original_checks
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
class TestHealthCheckScenarios:
|
|
205
|
-
"""Test real-world health check scenarios."""
|
|
206
|
-
|
|
207
|
-
@pytest.mark.asyncio
|
|
208
|
-
async def test_multiple_services_all_healthy(self):
|
|
209
|
-
"""Test health check with multiple healthy services."""
|
|
210
|
-
hc = HealthCheck()
|
|
211
|
-
|
|
212
|
-
async def check_database():
|
|
213
|
-
return True, "Database OK"
|
|
214
|
-
|
|
215
|
-
async def check_redis():
|
|
216
|
-
return True, "Redis OK"
|
|
217
|
-
|
|
218
|
-
async def check_queue():
|
|
219
|
-
return True, "Queue OK"
|
|
220
|
-
|
|
221
|
-
hc.add_check(check_database)
|
|
222
|
-
hc.add_check(check_redis)
|
|
223
|
-
hc.add_check(check_queue)
|
|
224
|
-
|
|
225
|
-
results = await hc.run_checks()
|
|
226
|
-
assert results["status"] == "healthy"
|
|
227
|
-
assert len(results["checks"]) == 3
|
|
228
|
-
|
|
229
|
-
@pytest.mark.asyncio
|
|
230
|
-
async def test_multiple_services_one_degraded(self):
|
|
231
|
-
"""Test health check with one degraded service."""
|
|
232
|
-
hc = HealthCheck()
|
|
233
|
-
|
|
234
|
-
async def check_database():
|
|
235
|
-
return True, "Database OK"
|
|
236
|
-
|
|
237
|
-
async def check_redis():
|
|
238
|
-
return False, "Redis connection timeout"
|
|
239
|
-
|
|
240
|
-
async def check_queue():
|
|
241
|
-
return True, "Queue OK"
|
|
242
|
-
|
|
243
|
-
hc.add_check(check_database)
|
|
244
|
-
hc.add_check(check_redis)
|
|
245
|
-
hc.add_check(check_queue)
|
|
246
|
-
|
|
247
|
-
results = await hc.run_checks()
|
|
248
|
-
assert results["status"] == "unhealthy"
|
|
249
|
-
assert results["checks"]["check_database"]["healthy"] is True
|
|
250
|
-
assert results["checks"]["check_redis"]["healthy"] is False
|
|
251
|
-
assert results["checks"]["check_queue"]["healthy"] is True
|
|
252
|
-
|
|
253
|
-
@pytest.mark.asyncio
|
|
254
|
-
async def test_async_check_performance(self):
|
|
255
|
-
"""Test that async checks run concurrently."""
|
|
256
|
-
hc = HealthCheck()
|
|
257
|
-
import time
|
|
258
|
-
|
|
259
|
-
start_time = time.time()
|
|
260
|
-
|
|
261
|
-
async def slow_check1():
|
|
262
|
-
await asyncio.sleep(0.1)
|
|
263
|
-
return True, "Check 1 OK"
|
|
264
|
-
|
|
265
|
-
async def slow_check2():
|
|
266
|
-
await asyncio.sleep(0.1)
|
|
267
|
-
return True, "Check 2 OK"
|
|
268
|
-
|
|
269
|
-
hc.add_check(slow_check1)
|
|
270
|
-
hc.add_check(slow_check2)
|
|
271
|
-
|
|
272
|
-
results = await hc.run_checks()
|
|
273
|
-
elapsed = time.time() - start_time
|
|
274
|
-
|
|
275
|
-
# If checks run sequentially, would take ~0.2s
|
|
276
|
-
# If checks run concurrently, should take ~0.1s
|
|
277
|
-
# We're currently running sequentially, so this will be >0.15s
|
|
278
|
-
# TODO: Optimize to run checks concurrently
|
|
279
|
-
assert elapsed < 0.3 # Just check it completes reasonably fast
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if __name__ == "__main__":
|
|
283
|
-
pytest.main([__file__, "-v"])
|