django-bolt 0.1.0__cp310-abi3-win_amd64.whl → 0.1.2__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.2.dist-info}/METADATA +181 -201
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.2.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.2.dist-info}/WHEEL +0 -0
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.2.dist-info}/entry_points.txt +0 -0
|
@@ -1,837 +0,0 @@
|
|
|
1
|
-
"""Comprehensive tests for Django-Bolt logging system.
|
|
2
|
-
|
|
3
|
-
These tests validate the behavior documented in docs/LOGGING.md:
|
|
4
|
-
- Queue-based non-blocking logging
|
|
5
|
-
- Production defaults (min_duration_ms, sample_rate)
|
|
6
|
-
- Request/response logging with proper log levels
|
|
7
|
-
- Skip paths and status codes
|
|
8
|
-
- Header/cookie obfuscation
|
|
9
|
-
- Integration with Django's logging system
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import pytest
|
|
13
|
-
import logging
|
|
14
|
-
import time
|
|
15
|
-
from unittest.mock import Mock, patch, MagicMock
|
|
16
|
-
from logging.handlers import QueueHandler
|
|
17
|
-
from django_bolt.logging import LoggingConfig, LoggingMiddleware, create_logging_middleware
|
|
18
|
-
from django_bolt.logging.config import setup_django_logging, _ensure_queue_logging
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class TestLoggingConfig:
|
|
22
|
-
"""Test LoggingConfig behavior documented in LOGGING.md."""
|
|
23
|
-
|
|
24
|
-
def test_default_config_uses_django_server_logger(self):
|
|
25
|
-
"""Default logger should be 'django.server' as documented."""
|
|
26
|
-
config = LoggingConfig()
|
|
27
|
-
assert config.logger_name == "django.server", "Default logger must be 'django.server' per LOGGING.md"
|
|
28
|
-
|
|
29
|
-
def test_default_request_fields_include_basic_info(self):
|
|
30
|
-
"""Default request fields should include method, path, and status_code."""
|
|
31
|
-
config = LoggingConfig()
|
|
32
|
-
assert "method" in config.request_log_fields
|
|
33
|
-
assert "path" in config.request_log_fields
|
|
34
|
-
assert "status_code" in config.request_log_fields
|
|
35
|
-
|
|
36
|
-
def test_default_response_fields_include_status_code(self):
|
|
37
|
-
"""Default response fields should include status_code."""
|
|
38
|
-
config = LoggingConfig()
|
|
39
|
-
assert "status_code" in config.response_log_fields
|
|
40
|
-
|
|
41
|
-
def test_default_security_headers_are_obfuscated(self):
|
|
42
|
-
"""Security-sensitive headers should be obfuscated by default."""
|
|
43
|
-
config = LoggingConfig()
|
|
44
|
-
assert "authorization" in config.obfuscate_headers
|
|
45
|
-
assert "cookie" in config.obfuscate_headers
|
|
46
|
-
assert "x-api-key" in config.obfuscate_headers
|
|
47
|
-
assert "x-auth-token" in config.obfuscate_headers
|
|
48
|
-
|
|
49
|
-
def test_default_security_cookies_are_obfuscated(self):
|
|
50
|
-
"""Security-sensitive cookies should be obfuscated by default."""
|
|
51
|
-
config = LoggingConfig()
|
|
52
|
-
assert "sessionid" in config.obfuscate_cookies
|
|
53
|
-
assert "csrftoken" in config.obfuscate_cookies
|
|
54
|
-
|
|
55
|
-
def test_default_skip_paths_include_health_checks(self):
|
|
56
|
-
"""Health check paths should be skipped by default."""
|
|
57
|
-
config = LoggingConfig()
|
|
58
|
-
assert "/health" in config.skip_paths
|
|
59
|
-
assert "/ready" in config.skip_paths
|
|
60
|
-
assert "/metrics" in config.skip_paths
|
|
61
|
-
|
|
62
|
-
def test_custom_logger_name(self):
|
|
63
|
-
"""Custom logger name should be configurable."""
|
|
64
|
-
config = LoggingConfig(logger_name="custom.logger")
|
|
65
|
-
assert config.logger_name == "custom.logger"
|
|
66
|
-
|
|
67
|
-
def test_custom_request_fields(self):
|
|
68
|
-
"""Custom request fields should be configurable."""
|
|
69
|
-
config = LoggingConfig(
|
|
70
|
-
request_log_fields={"method", "path", "body", "client_ip"}
|
|
71
|
-
)
|
|
72
|
-
assert "body" in config.request_log_fields
|
|
73
|
-
assert "client_ip" in config.request_log_fields
|
|
74
|
-
|
|
75
|
-
def test_custom_response_fields(self):
|
|
76
|
-
"""Custom response fields should be configurable."""
|
|
77
|
-
config = LoggingConfig(
|
|
78
|
-
response_log_fields={"status_code", "duration", "size"}
|
|
79
|
-
)
|
|
80
|
-
assert "status_code" in config.response_log_fields
|
|
81
|
-
assert "duration" in config.response_log_fields
|
|
82
|
-
assert "size" in config.response_log_fields
|
|
83
|
-
|
|
84
|
-
def test_sample_rate_configuration(self):
|
|
85
|
-
"""Sample rate should be configurable for reducing 2xx/3xx log volume."""
|
|
86
|
-
config = LoggingConfig(sample_rate=0.05)
|
|
87
|
-
assert config.sample_rate == 0.05
|
|
88
|
-
|
|
89
|
-
def test_min_duration_ms_configuration(self):
|
|
90
|
-
"""Minimum duration threshold should be configurable for slow-only logging."""
|
|
91
|
-
config = LoggingConfig(min_duration_ms=500)
|
|
92
|
-
assert config.min_duration_ms == 500
|
|
93
|
-
|
|
94
|
-
def test_skip_paths_configuration(self):
|
|
95
|
-
"""Skip paths should be fully customizable."""
|
|
96
|
-
config = LoggingConfig(skip_paths={"/admin", "/static"})
|
|
97
|
-
assert "/admin" in config.skip_paths
|
|
98
|
-
assert "/static" in config.skip_paths
|
|
99
|
-
|
|
100
|
-
def test_skip_status_codes_configuration(self):
|
|
101
|
-
"""Skip status codes should be configurable."""
|
|
102
|
-
config = LoggingConfig(skip_status_codes={204, 304})
|
|
103
|
-
assert 204 in config.skip_status_codes
|
|
104
|
-
assert 304 in config.skip_status_codes
|
|
105
|
-
|
|
106
|
-
def test_should_log_request_returns_true_for_normal_paths(self):
|
|
107
|
-
"""Normal paths should be logged."""
|
|
108
|
-
config = LoggingConfig(skip_paths={"/health"})
|
|
109
|
-
assert config.should_log_request("/api/users") is True
|
|
110
|
-
|
|
111
|
-
def test_should_log_request_returns_false_for_skipped_paths(self):
|
|
112
|
-
"""Paths in skip_paths should not be logged."""
|
|
113
|
-
config = LoggingConfig(skip_paths={"/health", "/metrics"})
|
|
114
|
-
assert config.should_log_request("/health") is False
|
|
115
|
-
assert config.should_log_request("/metrics") is False
|
|
116
|
-
|
|
117
|
-
def test_should_log_request_returns_false_for_skipped_status_codes(self):
|
|
118
|
-
"""Status codes in skip_status_codes should not be logged."""
|
|
119
|
-
config = LoggingConfig(skip_status_codes={204, 304})
|
|
120
|
-
assert config.should_log_request("/api/users", 204) is False
|
|
121
|
-
assert config.should_log_request("/api/users", 304) is False
|
|
122
|
-
|
|
123
|
-
def test_should_log_request_returns_true_for_non_skipped_status_codes(self):
|
|
124
|
-
"""Non-skipped status codes should be logged."""
|
|
125
|
-
config = LoggingConfig(skip_status_codes={204, 304})
|
|
126
|
-
assert config.should_log_request("/api/users", 200) is True
|
|
127
|
-
assert config.should_log_request("/api/users", 500) is True
|
|
128
|
-
|
|
129
|
-
def test_get_logger_returns_logger_instance(self):
|
|
130
|
-
"""get_logger() should return a proper Logger instance."""
|
|
131
|
-
config = LoggingConfig(logger_name="test.logger")
|
|
132
|
-
logger = config.get_logger()
|
|
133
|
-
assert isinstance(logger, logging.Logger)
|
|
134
|
-
assert logger.name == "test.logger"
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
class TestLoggingMiddleware:
|
|
138
|
-
"""Test LoggingMiddleware behavior."""
|
|
139
|
-
|
|
140
|
-
def test_middleware_uses_default_config_when_none_provided(self):
|
|
141
|
-
"""Middleware should use default config when none is provided."""
|
|
142
|
-
middleware = LoggingMiddleware()
|
|
143
|
-
assert middleware.config is not None
|
|
144
|
-
assert middleware.logger is not None
|
|
145
|
-
|
|
146
|
-
def test_middleware_uses_provided_config(self):
|
|
147
|
-
"""Middleware should use provided config."""
|
|
148
|
-
config = LoggingConfig(logger_name="custom.logger")
|
|
149
|
-
middleware = LoggingMiddleware(config)
|
|
150
|
-
assert middleware.config.logger_name == "custom.logger"
|
|
151
|
-
|
|
152
|
-
def test_obfuscate_headers_masks_sensitive_headers(self):
|
|
153
|
-
"""Sensitive headers should be obfuscated to '***'."""
|
|
154
|
-
config = LoggingConfig(obfuscate_headers={"authorization", "x-api-key"})
|
|
155
|
-
middleware = LoggingMiddleware(config)
|
|
156
|
-
|
|
157
|
-
headers = {
|
|
158
|
-
"content-type": "application/json",
|
|
159
|
-
"authorization": "Bearer secret-token-12345",
|
|
160
|
-
"x-api-key": "my-secret-key",
|
|
161
|
-
"user-agent": "test-agent",
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
obfuscated = middleware.obfuscate_headers(headers)
|
|
165
|
-
assert obfuscated["content-type"] == "application/json"
|
|
166
|
-
assert obfuscated["authorization"] == "***", "Authorization header must be obfuscated"
|
|
167
|
-
assert obfuscated["x-api-key"] == "***", "X-API-Key header must be obfuscated"
|
|
168
|
-
assert obfuscated["user-agent"] == "test-agent"
|
|
169
|
-
|
|
170
|
-
def test_obfuscate_headers_is_case_insensitive(self):
|
|
171
|
-
"""Header obfuscation should be case-insensitive."""
|
|
172
|
-
config = LoggingConfig(obfuscate_headers={"authorization"})
|
|
173
|
-
middleware = LoggingMiddleware(config)
|
|
174
|
-
|
|
175
|
-
headers = {
|
|
176
|
-
"Authorization": "Bearer token",
|
|
177
|
-
"AUTHORIZATION": "Bearer token2",
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
obfuscated = middleware.obfuscate_headers(headers)
|
|
181
|
-
assert obfuscated["Authorization"] == "***"
|
|
182
|
-
assert obfuscated["AUTHORIZATION"] == "***"
|
|
183
|
-
|
|
184
|
-
def test_obfuscate_cookies_masks_sensitive_cookies(self):
|
|
185
|
-
"""Sensitive cookies should be obfuscated to '***'."""
|
|
186
|
-
config = LoggingConfig(obfuscate_cookies={"sessionid", "csrftoken"})
|
|
187
|
-
middleware = LoggingMiddleware(config)
|
|
188
|
-
|
|
189
|
-
cookies = {
|
|
190
|
-
"sessionid": "abc123xyz789",
|
|
191
|
-
"csrftoken": "token456def",
|
|
192
|
-
"preferences": "dark-mode",
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
obfuscated = middleware.obfuscate_cookies(cookies)
|
|
196
|
-
assert obfuscated["sessionid"] == "***", "sessionid cookie must be obfuscated"
|
|
197
|
-
assert obfuscated["csrftoken"] == "***", "csrftoken cookie must be obfuscated"
|
|
198
|
-
assert obfuscated["preferences"] == "dark-mode"
|
|
199
|
-
|
|
200
|
-
def test_extract_request_data_includes_configured_fields_only(self):
|
|
201
|
-
"""Only configured request fields should be extracted."""
|
|
202
|
-
config = LoggingConfig(request_log_fields={"method", "path"})
|
|
203
|
-
middleware = LoggingMiddleware(config)
|
|
204
|
-
|
|
205
|
-
request = {
|
|
206
|
-
"method": "POST",
|
|
207
|
-
"path": "/api/users",
|
|
208
|
-
"query_params": {"page": "1"},
|
|
209
|
-
"headers": {"content-type": "application/json"},
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
data = middleware.extract_request_data(request)
|
|
213
|
-
assert "method" in data
|
|
214
|
-
assert "path" in data
|
|
215
|
-
assert "query" not in data, "Query should not be included when not configured"
|
|
216
|
-
assert "headers" not in data, "Headers should not be included when not configured"
|
|
217
|
-
|
|
218
|
-
def test_extract_request_data_includes_query_params_when_configured(self):
|
|
219
|
-
"""Query params should be included when configured."""
|
|
220
|
-
config = LoggingConfig(request_log_fields={"method", "path", "query"})
|
|
221
|
-
middleware = LoggingMiddleware(config)
|
|
222
|
-
|
|
223
|
-
request = {
|
|
224
|
-
"method": "GET",
|
|
225
|
-
"path": "/api/users",
|
|
226
|
-
"query_params": {"page": "1", "limit": "10"},
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
data = middleware.extract_request_data(request)
|
|
230
|
-
assert "query" in data
|
|
231
|
-
assert data["query"] == {"page": "1", "limit": "10"}
|
|
232
|
-
|
|
233
|
-
def test_extract_request_data_obfuscates_headers(self):
|
|
234
|
-
"""Headers should be obfuscated when included."""
|
|
235
|
-
config = LoggingConfig(
|
|
236
|
-
request_log_fields={"method", "path", "headers"},
|
|
237
|
-
obfuscate_headers={"authorization"},
|
|
238
|
-
)
|
|
239
|
-
middleware = LoggingMiddleware(config)
|
|
240
|
-
|
|
241
|
-
request = {
|
|
242
|
-
"method": "POST",
|
|
243
|
-
"path": "/api/users",
|
|
244
|
-
"headers": {
|
|
245
|
-
"content-type": "application/json",
|
|
246
|
-
"authorization": "Bearer secret-token",
|
|
247
|
-
},
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
data = middleware.extract_request_data(request)
|
|
251
|
-
assert "headers" in data
|
|
252
|
-
assert data["headers"]["authorization"] == "***", "Authorization must be obfuscated"
|
|
253
|
-
assert data["headers"]["content-type"] == "application/json"
|
|
254
|
-
|
|
255
|
-
def test_extract_request_data_includes_body_when_configured(self):
|
|
256
|
-
"""Request body should be included when log_request_body=True."""
|
|
257
|
-
config = LoggingConfig(
|
|
258
|
-
request_log_fields={"method", "path", "body"},
|
|
259
|
-
log_request_body=True,
|
|
260
|
-
max_body_log_size=100,
|
|
261
|
-
)
|
|
262
|
-
middleware = LoggingMiddleware(config)
|
|
263
|
-
|
|
264
|
-
request = {
|
|
265
|
-
"method": "POST",
|
|
266
|
-
"path": "/api/users",
|
|
267
|
-
"body": b'{"name": "test", "email": "test@example.com"}',
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
data = middleware.extract_request_data(request)
|
|
271
|
-
assert "body" in data
|
|
272
|
-
assert "test@example.com" in data["body"]
|
|
273
|
-
|
|
274
|
-
def test_extract_request_data_skips_body_when_too_large(self):
|
|
275
|
-
"""Request body should be skipped when exceeding max_body_log_size."""
|
|
276
|
-
config = LoggingConfig(
|
|
277
|
-
request_log_fields={"method", "path", "body"},
|
|
278
|
-
log_request_body=True,
|
|
279
|
-
max_body_log_size=10,
|
|
280
|
-
)
|
|
281
|
-
middleware = LoggingMiddleware(config)
|
|
282
|
-
|
|
283
|
-
request = {
|
|
284
|
-
"method": "POST",
|
|
285
|
-
"path": "/api/users",
|
|
286
|
-
"body": b'{"name": "test", "email": "test@example.com"}', # >10 bytes
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
data = middleware.extract_request_data(request)
|
|
290
|
-
assert "body" not in data, "Body should not be included when too large"
|
|
291
|
-
|
|
292
|
-
def test_extract_request_data_handles_binary_body(self):
|
|
293
|
-
"""Binary body should be logged with size info."""
|
|
294
|
-
config = LoggingConfig(
|
|
295
|
-
request_log_fields={"method", "path", "body"},
|
|
296
|
-
log_request_body=True,
|
|
297
|
-
max_body_log_size=100,
|
|
298
|
-
)
|
|
299
|
-
middleware = LoggingMiddleware(config)
|
|
300
|
-
|
|
301
|
-
request = {
|
|
302
|
-
"method": "POST",
|
|
303
|
-
"path": "/api/upload",
|
|
304
|
-
"body": b'\xff\xfe\x00\x01', # Binary data
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
data = middleware.extract_request_data(request)
|
|
308
|
-
assert "body" in data
|
|
309
|
-
assert "<binary data," in data["body"]
|
|
310
|
-
assert "bytes>" in data["body"]
|
|
311
|
-
|
|
312
|
-
def test_extract_request_data_extracts_client_ip_from_x_forwarded_for(self):
|
|
313
|
-
"""Client IP should be extracted from X-Forwarded-For header."""
|
|
314
|
-
config = LoggingConfig(request_log_fields={"client_ip"})
|
|
315
|
-
middleware = LoggingMiddleware(config)
|
|
316
|
-
|
|
317
|
-
request = {
|
|
318
|
-
"method": "GET",
|
|
319
|
-
"path": "/api/users",
|
|
320
|
-
"headers": {"x-forwarded-for": "192.168.1.1, 10.0.0.1"},
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
data = middleware.extract_request_data(request)
|
|
324
|
-
assert data["client_ip"] == "192.168.1.1", "Should extract first IP from X-Forwarded-For"
|
|
325
|
-
|
|
326
|
-
def test_extract_request_data_extracts_client_ip_from_x_real_ip(self):
|
|
327
|
-
"""Client IP should be extracted from X-Real-IP header."""
|
|
328
|
-
config = LoggingConfig(request_log_fields={"client_ip"})
|
|
329
|
-
middleware = LoggingMiddleware(config)
|
|
330
|
-
|
|
331
|
-
request = {
|
|
332
|
-
"method": "GET",
|
|
333
|
-
"path": "/api/users",
|
|
334
|
-
"headers": {"x-real-ip": "192.168.1.2"},
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
data = middleware.extract_request_data(request)
|
|
338
|
-
assert data["client_ip"] == "192.168.1.2"
|
|
339
|
-
|
|
340
|
-
def test_extract_request_data_extracts_user_agent(self):
|
|
341
|
-
"""User agent should be extracted when configured."""
|
|
342
|
-
config = LoggingConfig(request_log_fields={"user_agent"})
|
|
343
|
-
middleware = LoggingMiddleware(config)
|
|
344
|
-
|
|
345
|
-
request = {
|
|
346
|
-
"method": "GET",
|
|
347
|
-
"path": "/api/users",
|
|
348
|
-
"headers": {"user-agent": "Mozilla/5.0"},
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
data = middleware.extract_request_data(request)
|
|
352
|
-
assert data["user_agent"] == "Mozilla/5.0"
|
|
353
|
-
|
|
354
|
-
def test_log_request_skips_when_path_in_skip_paths(self, caplog):
|
|
355
|
-
"""Requests to skip_paths should not be logged."""
|
|
356
|
-
config = LoggingConfig(skip_paths={"/health"})
|
|
357
|
-
middleware = LoggingMiddleware(config)
|
|
358
|
-
|
|
359
|
-
request = {"method": "GET", "path": "/health"}
|
|
360
|
-
|
|
361
|
-
with caplog.at_level(logging.DEBUG):
|
|
362
|
-
middleware.log_request(request)
|
|
363
|
-
|
|
364
|
-
assert len(caplog.records) == 0, "Health checks should not be logged"
|
|
365
|
-
|
|
366
|
-
def test_log_request_logs_when_debug_enabled(self, caplog):
|
|
367
|
-
"""Requests should be logged at DEBUG level when logger is enabled for DEBUG."""
|
|
368
|
-
config = LoggingConfig(logger_name="test.logger")
|
|
369
|
-
middleware = LoggingMiddleware(config)
|
|
370
|
-
|
|
371
|
-
request = {"method": "GET", "path": "/api/users"}
|
|
372
|
-
|
|
373
|
-
with caplog.at_level(logging.DEBUG, logger="test.logger"):
|
|
374
|
-
middleware.log_request(request)
|
|
375
|
-
|
|
376
|
-
# Should have logged at DEBUG level
|
|
377
|
-
assert len(caplog.records) > 0, "Request should be logged at DEBUG level"
|
|
378
|
-
assert caplog.records[0].levelno == logging.DEBUG
|
|
379
|
-
|
|
380
|
-
def test_log_request_skips_when_debug_disabled(self, caplog):
|
|
381
|
-
"""Requests should not be logged when logger is not enabled for DEBUG."""
|
|
382
|
-
config = LoggingConfig(logger_name="test.logger")
|
|
383
|
-
middleware = LoggingMiddleware(config)
|
|
384
|
-
|
|
385
|
-
request = {"method": "GET", "path": "/api/users"}
|
|
386
|
-
|
|
387
|
-
# Set level to INFO (higher than DEBUG)
|
|
388
|
-
with caplog.at_level(logging.INFO, logger="test.logger"):
|
|
389
|
-
middleware.log_request(request)
|
|
390
|
-
|
|
391
|
-
# Should not log because DEBUG is disabled
|
|
392
|
-
assert len(caplog.records) == 0, "Request should not be logged when DEBUG is disabled"
|
|
393
|
-
|
|
394
|
-
def test_log_response_uses_info_level_for_2xx(self, caplog):
|
|
395
|
-
"""Successful 2xx responses should be logged at INFO level."""
|
|
396
|
-
config = LoggingConfig(logger_name="test.logger")
|
|
397
|
-
middleware = LoggingMiddleware(config)
|
|
398
|
-
|
|
399
|
-
request = {"method": "POST", "path": "/api/users"}
|
|
400
|
-
|
|
401
|
-
with caplog.at_level(logging.INFO, logger="test.logger"):
|
|
402
|
-
middleware.log_response(request, 201, 0.1)
|
|
403
|
-
|
|
404
|
-
assert len(caplog.records) > 0, "2xx response should be logged"
|
|
405
|
-
assert caplog.records[0].levelno == logging.INFO
|
|
406
|
-
|
|
407
|
-
def test_log_response_uses_info_level_for_3xx(self, caplog):
|
|
408
|
-
"""Redirect 3xx responses should be logged at INFO level."""
|
|
409
|
-
config = LoggingConfig(logger_name="test.logger")
|
|
410
|
-
middleware = LoggingMiddleware(config)
|
|
411
|
-
|
|
412
|
-
request = {"method": "GET", "path": "/api/redirect"}
|
|
413
|
-
|
|
414
|
-
with caplog.at_level(logging.INFO, logger="test.logger"):
|
|
415
|
-
middleware.log_response(request, 302, 0.05)
|
|
416
|
-
|
|
417
|
-
assert len(caplog.records) > 0, "3xx response should be logged"
|
|
418
|
-
assert caplog.records[0].levelno == logging.INFO
|
|
419
|
-
|
|
420
|
-
def test_log_response_uses_warning_level_for_4xx(self, caplog):
|
|
421
|
-
"""Client error 4xx responses should be logged at WARNING level."""
|
|
422
|
-
config = LoggingConfig(logger_name="test.logger")
|
|
423
|
-
middleware = LoggingMiddleware(config)
|
|
424
|
-
|
|
425
|
-
request = {"method": "GET", "path": "/api/notfound"}
|
|
426
|
-
|
|
427
|
-
with caplog.at_level(logging.WARNING, logger="test.logger"):
|
|
428
|
-
middleware.log_response(request, 404, 0.05)
|
|
429
|
-
|
|
430
|
-
assert len(caplog.records) > 0, "4xx response should be logged"
|
|
431
|
-
assert caplog.records[0].levelno == logging.WARNING
|
|
432
|
-
|
|
433
|
-
def test_log_response_uses_error_level_for_5xx(self, caplog):
|
|
434
|
-
"""Server error 5xx responses should be logged at ERROR level."""
|
|
435
|
-
config = LoggingConfig(logger_name="test.logger")
|
|
436
|
-
middleware = LoggingMiddleware(config)
|
|
437
|
-
|
|
438
|
-
request = {"method": "GET", "path": "/api/error"}
|
|
439
|
-
|
|
440
|
-
with caplog.at_level(logging.ERROR, logger="test.logger"):
|
|
441
|
-
middleware.log_response(request, 500, 0.1)
|
|
442
|
-
|
|
443
|
-
assert len(caplog.records) > 0, "5xx response should be logged"
|
|
444
|
-
assert caplog.records[0].levelno == logging.ERROR
|
|
445
|
-
|
|
446
|
-
def test_log_response_skips_for_skip_paths(self, caplog):
|
|
447
|
-
"""Responses for skip_paths should not be logged."""
|
|
448
|
-
config = LoggingConfig(skip_paths={"/metrics"})
|
|
449
|
-
middleware = LoggingMiddleware(config)
|
|
450
|
-
|
|
451
|
-
request = {"method": "GET", "path": "/metrics"}
|
|
452
|
-
|
|
453
|
-
with caplog.at_level(logging.INFO):
|
|
454
|
-
middleware.log_response(request, 200, 0.01)
|
|
455
|
-
|
|
456
|
-
assert len(caplog.records) == 0, "Metrics endpoint should not be logged"
|
|
457
|
-
|
|
458
|
-
def test_log_response_skips_for_skip_status_codes(self, caplog):
|
|
459
|
-
"""Responses with skip_status_codes should not be logged."""
|
|
460
|
-
config = LoggingConfig(skip_status_codes={204})
|
|
461
|
-
middleware = LoggingMiddleware(config)
|
|
462
|
-
|
|
463
|
-
request = {"method": "DELETE", "path": "/api/users/1"}
|
|
464
|
-
|
|
465
|
-
with caplog.at_level(logging.INFO):
|
|
466
|
-
middleware.log_response(request, 204, 0.01)
|
|
467
|
-
|
|
468
|
-
assert len(caplog.records) == 0, "204 responses should not be logged when configured"
|
|
469
|
-
|
|
470
|
-
def test_log_response_skips_when_info_disabled_for_2xx(self, caplog):
|
|
471
|
-
"""2xx responses should not be logged when logger is not enabled for INFO."""
|
|
472
|
-
config = LoggingConfig(logger_name="test.logger")
|
|
473
|
-
middleware = LoggingMiddleware(config)
|
|
474
|
-
|
|
475
|
-
request = {"method": "GET", "path": "/api/users"}
|
|
476
|
-
|
|
477
|
-
# Set level to ERROR (higher than INFO)
|
|
478
|
-
with caplog.at_level(logging.ERROR, logger="test.logger"):
|
|
479
|
-
middleware.log_response(request, 200, 0.1)
|
|
480
|
-
|
|
481
|
-
# Should not log 2xx when INFO is disabled
|
|
482
|
-
assert len(caplog.records) == 0, "2xx should not be logged when INFO is disabled"
|
|
483
|
-
|
|
484
|
-
def test_log_response_always_logs_4xx_regardless_of_level(self, caplog):
|
|
485
|
-
"""4xx errors should always be logged even if WARNING is disabled (they use WARNING level)."""
|
|
486
|
-
config = LoggingConfig(logger_name="test.logger")
|
|
487
|
-
middleware = LoggingMiddleware(config)
|
|
488
|
-
|
|
489
|
-
request = {"method": "GET", "path": "/api/notfound"}
|
|
490
|
-
|
|
491
|
-
# Set level to WARNING to capture WARNING logs
|
|
492
|
-
with caplog.at_level(logging.WARNING, logger="test.logger"):
|
|
493
|
-
middleware.log_response(request, 404, 0.05)
|
|
494
|
-
|
|
495
|
-
# 4xx should be logged at WARNING level
|
|
496
|
-
assert len(caplog.records) > 0, "4xx should be logged at WARNING level"
|
|
497
|
-
|
|
498
|
-
def test_log_response_always_logs_5xx_regardless_of_level(self, caplog):
|
|
499
|
-
"""5xx errors should always be logged at ERROR level."""
|
|
500
|
-
config = LoggingConfig(logger_name="test.logger")
|
|
501
|
-
middleware = LoggingMiddleware(config)
|
|
502
|
-
|
|
503
|
-
request = {"method": "GET", "path": "/api/error"}
|
|
504
|
-
|
|
505
|
-
with caplog.at_level(logging.ERROR, logger="test.logger"):
|
|
506
|
-
middleware.log_response(request, 500, 0.1)
|
|
507
|
-
|
|
508
|
-
# 5xx should be logged at ERROR level
|
|
509
|
-
assert len(caplog.records) > 0, "5xx should be logged at ERROR level"
|
|
510
|
-
|
|
511
|
-
def test_log_response_respects_sample_rate_for_2xx(self):
|
|
512
|
-
"""Sample rate should reduce 2xx/3xx log volume probabilistically."""
|
|
513
|
-
config = LoggingConfig(sample_rate=0.0, logger_name="test.logger")
|
|
514
|
-
middleware = LoggingMiddleware(config)
|
|
515
|
-
|
|
516
|
-
request = {"method": "GET", "path": "/api/users"}
|
|
517
|
-
|
|
518
|
-
# With sample_rate=0.0, no 2xx responses should be logged
|
|
519
|
-
logged_count = 0
|
|
520
|
-
for _ in range(100):
|
|
521
|
-
logger = logging.getLogger("test.logger")
|
|
522
|
-
logger.handlers.clear()
|
|
523
|
-
handler = logging.Handler()
|
|
524
|
-
handler.handle = Mock()
|
|
525
|
-
logger.addHandler(handler)
|
|
526
|
-
logger.setLevel(logging.INFO)
|
|
527
|
-
|
|
528
|
-
middleware.log_response(request, 200, 0.1)
|
|
529
|
-
|
|
530
|
-
if handler.handle.called:
|
|
531
|
-
logged_count += 1
|
|
532
|
-
|
|
533
|
-
assert logged_count == 0, "With sample_rate=0.0, no 2xx responses should be logged"
|
|
534
|
-
|
|
535
|
-
def test_log_response_does_not_sample_errors(self, caplog):
|
|
536
|
-
"""Sample rate should NOT affect 4xx/5xx errors - they should always be logged."""
|
|
537
|
-
config = LoggingConfig(sample_rate=0.0, logger_name="test.logger")
|
|
538
|
-
middleware = LoggingMiddleware(config)
|
|
539
|
-
|
|
540
|
-
request = {"method": "GET", "path": "/api/error"}
|
|
541
|
-
|
|
542
|
-
# Even with sample_rate=0.0, errors should be logged
|
|
543
|
-
with caplog.at_level(logging.ERROR, logger="test.logger"):
|
|
544
|
-
middleware.log_response(request, 500, 0.1)
|
|
545
|
-
|
|
546
|
-
assert len(caplog.records) > 0, "Errors should be logged regardless of sample_rate"
|
|
547
|
-
|
|
548
|
-
def test_log_response_respects_min_duration_ms_for_2xx(self, caplog):
|
|
549
|
-
"""Fast 2xx responses should be skipped when below min_duration_ms threshold."""
|
|
550
|
-
config = LoggingConfig(min_duration_ms=250, logger_name="test.logger")
|
|
551
|
-
middleware = LoggingMiddleware(config)
|
|
552
|
-
|
|
553
|
-
request = {"method": "GET", "path": "/api/users"}
|
|
554
|
-
|
|
555
|
-
# Fast response (100ms < 250ms)
|
|
556
|
-
with caplog.at_level(logging.INFO, logger="test.logger"):
|
|
557
|
-
middleware.log_response(request, 200, 0.1)
|
|
558
|
-
|
|
559
|
-
assert len(caplog.records) == 0, "Fast 2xx responses should not be logged with min_duration_ms"
|
|
560
|
-
|
|
561
|
-
def test_log_response_logs_slow_2xx_above_min_duration_ms(self, caplog):
|
|
562
|
-
"""Slow 2xx responses should be logged when above min_duration_ms threshold."""
|
|
563
|
-
config = LoggingConfig(min_duration_ms=250, logger_name="test.logger")
|
|
564
|
-
middleware = LoggingMiddleware(config)
|
|
565
|
-
|
|
566
|
-
request = {"method": "GET", "path": "/api/users"}
|
|
567
|
-
|
|
568
|
-
# Slow response (300ms > 250ms)
|
|
569
|
-
with caplog.at_level(logging.INFO, logger="test.logger"):
|
|
570
|
-
middleware.log_response(request, 200, 0.3)
|
|
571
|
-
|
|
572
|
-
assert len(caplog.records) > 0, "Slow 2xx responses should be logged"
|
|
573
|
-
|
|
574
|
-
def test_log_response_ignores_min_duration_ms_for_errors(self, caplog):
|
|
575
|
-
"""min_duration_ms should NOT affect 4xx/5xx errors - they should always be logged."""
|
|
576
|
-
config = LoggingConfig(min_duration_ms=250, logger_name="test.logger")
|
|
577
|
-
middleware = LoggingMiddleware(config)
|
|
578
|
-
|
|
579
|
-
request = {"method": "GET", "path": "/api/error"}
|
|
580
|
-
|
|
581
|
-
# Fast error (50ms < 250ms)
|
|
582
|
-
with caplog.at_level(logging.ERROR, logger="test.logger"):
|
|
583
|
-
middleware.log_response(request, 500, 0.05)
|
|
584
|
-
|
|
585
|
-
assert len(caplog.records) > 0, "Errors should be logged regardless of min_duration_ms"
|
|
586
|
-
|
|
587
|
-
def test_log_response_includes_duration_in_milliseconds(self, caplog):
|
|
588
|
-
"""Duration should be logged in milliseconds when configured."""
|
|
589
|
-
config = LoggingConfig(
|
|
590
|
-
logger_name="test.logger",
|
|
591
|
-
response_log_fields={"status_code", "duration"}
|
|
592
|
-
)
|
|
593
|
-
middleware = LoggingMiddleware(config)
|
|
594
|
-
|
|
595
|
-
request = {"method": "GET", "path": "/api/users"}
|
|
596
|
-
|
|
597
|
-
with caplog.at_level(logging.INFO, logger="test.logger"):
|
|
598
|
-
middleware.log_response(request, 200, 0.123)
|
|
599
|
-
|
|
600
|
-
assert len(caplog.records) > 0
|
|
601
|
-
log_message = caplog.records[0].message
|
|
602
|
-
assert "123" in log_message, "Duration should be in milliseconds"
|
|
603
|
-
assert "ms" in log_message
|
|
604
|
-
|
|
605
|
-
def test_log_response_includes_response_size_when_configured(self, caplog):
|
|
606
|
-
"""Response size should be logged when configured."""
|
|
607
|
-
config = LoggingConfig(
|
|
608
|
-
logger_name="test.logger",
|
|
609
|
-
response_log_fields={"status_code", "size"}
|
|
610
|
-
)
|
|
611
|
-
middleware = LoggingMiddleware(config)
|
|
612
|
-
|
|
613
|
-
request = {"method": "GET", "path": "/api/users"}
|
|
614
|
-
|
|
615
|
-
with caplog.at_level(logging.INFO, logger="test.logger"):
|
|
616
|
-
middleware.log_response(request, 200, 0.1, response_size=1024)
|
|
617
|
-
|
|
618
|
-
assert len(caplog.records) > 0
|
|
619
|
-
assert caplog.records[0].response_size == 1024
|
|
620
|
-
|
|
621
|
-
def test_log_exception_logs_at_error_level(self, caplog):
|
|
622
|
-
"""Exceptions should be logged at ERROR level by default."""
|
|
623
|
-
config = LoggingConfig(logger_name="test.logger")
|
|
624
|
-
middleware = LoggingMiddleware(config)
|
|
625
|
-
|
|
626
|
-
request = {"method": "GET", "path": "/api/error"}
|
|
627
|
-
exc = ValueError("Something went wrong")
|
|
628
|
-
|
|
629
|
-
with caplog.at_level(logging.ERROR, logger="test.logger"):
|
|
630
|
-
middleware.log_exception(request, exc, exc_info=False)
|
|
631
|
-
|
|
632
|
-
assert len(caplog.records) > 0, "Exception should be logged"
|
|
633
|
-
assert caplog.records[0].levelno == logging.ERROR
|
|
634
|
-
|
|
635
|
-
def test_log_exception_includes_exception_details(self, caplog):
|
|
636
|
-
"""Exception logs should include exception type and message."""
|
|
637
|
-
config = LoggingConfig(logger_name="test.logger")
|
|
638
|
-
middleware = LoggingMiddleware(config)
|
|
639
|
-
|
|
640
|
-
request = {"method": "POST", "path": "/api/users"}
|
|
641
|
-
exc = ValueError("Invalid user data")
|
|
642
|
-
|
|
643
|
-
with caplog.at_level(logging.ERROR, logger="test.logger"):
|
|
644
|
-
middleware.log_exception(request, exc, exc_info=False)
|
|
645
|
-
|
|
646
|
-
assert len(caplog.records) > 0
|
|
647
|
-
record = caplog.records[0]
|
|
648
|
-
assert "ValueError" in record.message
|
|
649
|
-
assert "Invalid user data" in record.message
|
|
650
|
-
assert record.exception_type == "ValueError"
|
|
651
|
-
assert record.exception == "Invalid user data"
|
|
652
|
-
|
|
653
|
-
def test_log_exception_includes_request_context(self, caplog):
|
|
654
|
-
"""Exception logs should include request method and path."""
|
|
655
|
-
config = LoggingConfig(logger_name="test.logger")
|
|
656
|
-
middleware = LoggingMiddleware(config)
|
|
657
|
-
|
|
658
|
-
request = {"method": "DELETE", "path": "/api/users/123"}
|
|
659
|
-
exc = RuntimeError("Deletion failed")
|
|
660
|
-
|
|
661
|
-
with caplog.at_level(logging.ERROR, logger="test.logger"):
|
|
662
|
-
middleware.log_exception(request, exc, exc_info=False)
|
|
663
|
-
|
|
664
|
-
assert len(caplog.records) > 0
|
|
665
|
-
record = caplog.records[0]
|
|
666
|
-
assert record.method == "DELETE"
|
|
667
|
-
assert record.path == "/api/users/123"
|
|
668
|
-
|
|
669
|
-
def test_log_exception_uses_custom_handler_when_provided(self):
|
|
670
|
-
"""Custom exception handler should be called when provided."""
|
|
671
|
-
custom_handler = Mock()
|
|
672
|
-
config = LoggingConfig(
|
|
673
|
-
logger_name="test.logger",
|
|
674
|
-
exception_logging_handler=custom_handler
|
|
675
|
-
)
|
|
676
|
-
middleware = LoggingMiddleware(config)
|
|
677
|
-
|
|
678
|
-
request = {"method": "GET", "path": "/api/error"}
|
|
679
|
-
exc = Exception("Test error")
|
|
680
|
-
|
|
681
|
-
middleware.log_exception(request, exc, exc_info=True)
|
|
682
|
-
|
|
683
|
-
custom_handler.assert_called_once()
|
|
684
|
-
args = custom_handler.call_args[0]
|
|
685
|
-
assert args[1] == request # request
|
|
686
|
-
assert args[2] == exc # exception
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
class TestLoggingHelpers:
|
|
690
|
-
"""Test logging helper functions."""
|
|
691
|
-
|
|
692
|
-
def test_create_logging_middleware_with_custom_logger_name(self):
|
|
693
|
-
"""create_logging_middleware should accept custom logger name."""
|
|
694
|
-
middleware = create_logging_middleware(logger_name="custom.logger")
|
|
695
|
-
assert isinstance(middleware, LoggingMiddleware)
|
|
696
|
-
assert middleware.config.logger_name == "custom.logger"
|
|
697
|
-
|
|
698
|
-
def test_create_logging_middleware_with_custom_log_level(self):
|
|
699
|
-
"""create_logging_middleware should accept custom log level."""
|
|
700
|
-
middleware = create_logging_middleware(log_level="DEBUG")
|
|
701
|
-
assert isinstance(middleware, LoggingMiddleware)
|
|
702
|
-
assert middleware.config.log_level == "DEBUG"
|
|
703
|
-
|
|
704
|
-
def test_create_logging_middleware_with_kwargs(self):
|
|
705
|
-
"""create_logging_middleware should accept additional kwargs."""
|
|
706
|
-
middleware = create_logging_middleware(
|
|
707
|
-
skip_paths={"/custom"},
|
|
708
|
-
sample_rate=0.1
|
|
709
|
-
)
|
|
710
|
-
assert isinstance(middleware, LoggingMiddleware)
|
|
711
|
-
assert "/custom" in middleware.config.skip_paths
|
|
712
|
-
assert middleware.config.sample_rate == 0.1
|
|
713
|
-
|
|
714
|
-
def test_create_logging_middleware_uses_defaults_when_no_args(self):
|
|
715
|
-
"""create_logging_middleware should use defaults when no args provided."""
|
|
716
|
-
middleware = create_logging_middleware()
|
|
717
|
-
assert isinstance(middleware, LoggingMiddleware)
|
|
718
|
-
assert middleware.config is not None
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
class TestQueueBasedLogging:
|
|
722
|
-
"""Test queue-based non-blocking logging setup documented in LOGGING.md."""
|
|
723
|
-
|
|
724
|
-
def test_ensure_queue_logging_returns_queue_handler(self):
|
|
725
|
-
"""_ensure_queue_logging should return a QueueHandler."""
|
|
726
|
-
handler = _ensure_queue_logging("INFO")
|
|
727
|
-
assert isinstance(handler, QueueHandler), "Must return QueueHandler for non-blocking logging"
|
|
728
|
-
|
|
729
|
-
def test_ensure_queue_logging_creates_queue_listener(self):
|
|
730
|
-
"""_ensure_queue_logging should create and start a QueueListener."""
|
|
731
|
-
from django_bolt.logging.config import _QUEUE_LISTENER
|
|
732
|
-
|
|
733
|
-
# Reset global state for test
|
|
734
|
-
import django_bolt.logging.config as config_module
|
|
735
|
-
config_module._QUEUE_LISTENER = None
|
|
736
|
-
config_module._QUEUE = None
|
|
737
|
-
|
|
738
|
-
handler = _ensure_queue_logging("INFO")
|
|
739
|
-
|
|
740
|
-
# Should have created listener
|
|
741
|
-
assert config_module._QUEUE_LISTENER is not None, "QueueListener should be created"
|
|
742
|
-
assert config_module._QUEUE is not None, "Queue should be created"
|
|
743
|
-
|
|
744
|
-
def test_setup_django_logging_configures_queue_handlers(self):
|
|
745
|
-
"""setup_django_logging should configure queue handlers for django loggers."""
|
|
746
|
-
# Configure Django settings for test
|
|
747
|
-
from django.conf import settings
|
|
748
|
-
if not settings.configured:
|
|
749
|
-
settings.configure(
|
|
750
|
-
DEBUG=True,
|
|
751
|
-
SECRET_KEY='test-secret-key',
|
|
752
|
-
)
|
|
753
|
-
|
|
754
|
-
# Reset global state
|
|
755
|
-
import django_bolt.logging.config as config_module
|
|
756
|
-
config_module._LOGGING_CONFIGURED = False
|
|
757
|
-
config_module._QUEUE_LISTENER = None
|
|
758
|
-
config_module._QUEUE = None
|
|
759
|
-
|
|
760
|
-
# Clear existing handlers
|
|
761
|
-
for logger_name in ("django", "django.server", "django_bolt", ""):
|
|
762
|
-
logger = logging.getLogger(logger_name)
|
|
763
|
-
logger.handlers.clear()
|
|
764
|
-
|
|
765
|
-
setup_django_logging(force=True)
|
|
766
|
-
|
|
767
|
-
# Check that loggers have queue handlers
|
|
768
|
-
django_logger = logging.getLogger("django")
|
|
769
|
-
django_server_logger = logging.getLogger("django.server")
|
|
770
|
-
django_bolt_logger = logging.getLogger("django_bolt")
|
|
771
|
-
|
|
772
|
-
# All should have handlers after setup
|
|
773
|
-
assert len(django_logger.handlers) > 0, "django logger should have handlers"
|
|
774
|
-
assert len(django_server_logger.handlers) > 0, "django.server logger should have handlers"
|
|
775
|
-
assert len(django_bolt_logger.handlers) > 0, "django_bolt logger should have handlers"
|
|
776
|
-
|
|
777
|
-
# At least one should be a QueueHandler
|
|
778
|
-
root_logger = logging.getLogger()
|
|
779
|
-
has_queue_handler = any(isinstance(h, QueueHandler) for h in root_logger.handlers)
|
|
780
|
-
assert has_queue_handler, "Root logger should have a QueueHandler"
|
|
781
|
-
|
|
782
|
-
def test_setup_django_logging_respects_explicit_logging_config(self):
|
|
783
|
-
"""setup_django_logging should skip setup when LOGGING is explicitly configured."""
|
|
784
|
-
# Configure Django settings with explicit LOGGING
|
|
785
|
-
from django.conf import settings
|
|
786
|
-
if not settings.configured:
|
|
787
|
-
settings.configure(
|
|
788
|
-
DEBUG=True,
|
|
789
|
-
SECRET_KEY='test-secret-key',
|
|
790
|
-
LOGGING={"version": 1, "disable_existing_loggers": False},
|
|
791
|
-
)
|
|
792
|
-
|
|
793
|
-
# Reset global state
|
|
794
|
-
import django_bolt.logging.config as config_module
|
|
795
|
-
config_module._LOGGING_CONFIGURED = False
|
|
796
|
-
|
|
797
|
-
# Count handlers before
|
|
798
|
-
django_logger = logging.getLogger("django")
|
|
799
|
-
handlers_before = len(django_logger.handlers)
|
|
800
|
-
|
|
801
|
-
setup_django_logging(force=True)
|
|
802
|
-
|
|
803
|
-
# Should mark as configured
|
|
804
|
-
assert config_module._LOGGING_CONFIGURED is True
|
|
805
|
-
|
|
806
|
-
# Should not have added handlers (respects explicit LOGGING)
|
|
807
|
-
handlers_after = len(django_logger.handlers)
|
|
808
|
-
# With explicit LOGGING, we don't modify handlers
|
|
809
|
-
assert handlers_after == handlers_before, "Should not modify handlers with explicit LOGGING"
|
|
810
|
-
|
|
811
|
-
def test_setup_django_logging_is_idempotent(self):
|
|
812
|
-
"""setup_django_logging should not reconfigure when called multiple times."""
|
|
813
|
-
# Configure Django settings for test
|
|
814
|
-
from django.conf import settings
|
|
815
|
-
if not settings.configured:
|
|
816
|
-
settings.configure(
|
|
817
|
-
DEBUG=True,
|
|
818
|
-
SECRET_KEY='test-secret-key',
|
|
819
|
-
)
|
|
820
|
-
|
|
821
|
-
import django_bolt.logging.config as config_module
|
|
822
|
-
|
|
823
|
-
# First call
|
|
824
|
-
config_module._LOGGING_CONFIGURED = False
|
|
825
|
-
setup_django_logging(force=True)
|
|
826
|
-
first_configured = config_module._LOGGING_CONFIGURED
|
|
827
|
-
|
|
828
|
-
# Second call (should be no-op)
|
|
829
|
-
setup_django_logging()
|
|
830
|
-
second_configured = config_module._LOGGING_CONFIGURED
|
|
831
|
-
|
|
832
|
-
assert first_configured is True, "First call should configure logging"
|
|
833
|
-
assert second_configured is True, "Second call should preserve configured state"
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
if __name__ == "__main__":
|
|
837
|
-
pytest.main([__file__, "-v", "-s"])
|