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,419 @@
|
|
|
1
|
+
"""Integration tests for logging with merged APIs.
|
|
2
|
+
|
|
3
|
+
Tests that per-API logging configurations are preserved when multiple
|
|
4
|
+
BoltAPI instances are merged (like during autodiscovery in runbolt).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import logging
|
|
9
|
+
from django_bolt import BoltAPI
|
|
10
|
+
from django_bolt.logging import LoggingConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestMergedAPILoggingPreservation:
|
|
14
|
+
"""Test that merged APIs preserve per-API logging configs."""
|
|
15
|
+
|
|
16
|
+
def test_handler_maps_to_original_api_after_merge(self):
|
|
17
|
+
"""Each handler should map back to its original API instance after merge."""
|
|
18
|
+
# Create API 1 with custom logging
|
|
19
|
+
logging_config_1 = LoggingConfig(
|
|
20
|
+
logger_name="api1_logger",
|
|
21
|
+
request_log_fields={"path", "client_ip"},
|
|
22
|
+
response_log_fields={"status_code"},
|
|
23
|
+
)
|
|
24
|
+
api1 = BoltAPI(logging_config=logging_config_1)
|
|
25
|
+
|
|
26
|
+
@api1.get("/api1")
|
|
27
|
+
async def handler1():
|
|
28
|
+
return {"api": 1}
|
|
29
|
+
|
|
30
|
+
# Create API 2 with different logging
|
|
31
|
+
logging_config_2 = LoggingConfig(
|
|
32
|
+
logger_name="api2_logger",
|
|
33
|
+
request_log_fields={"method", "path"},
|
|
34
|
+
response_log_fields={"duration"},
|
|
35
|
+
)
|
|
36
|
+
api2 = BoltAPI(logging_config=logging_config_2)
|
|
37
|
+
|
|
38
|
+
@api2.get("/api2")
|
|
39
|
+
async def handler2():
|
|
40
|
+
return {"api": 2}
|
|
41
|
+
|
|
42
|
+
# Simulate merge (like runbolt does with handler_id renumbering)
|
|
43
|
+
merged = BoltAPI(enable_logging=False)
|
|
44
|
+
merged._handler_api_map = {}
|
|
45
|
+
|
|
46
|
+
next_handler_id = 0
|
|
47
|
+
|
|
48
|
+
# Merge routes from api1 (renumber handler_ids to avoid collisions)
|
|
49
|
+
for method, path, old_handler_id, handler in api1._routes:
|
|
50
|
+
new_handler_id = next_handler_id
|
|
51
|
+
next_handler_id += 1
|
|
52
|
+
merged._routes.append((method, path, new_handler_id, handler))
|
|
53
|
+
merged._handlers[new_handler_id] = handler
|
|
54
|
+
merged._handler_api_map[new_handler_id] = api1
|
|
55
|
+
if handler in api1._handler_meta:
|
|
56
|
+
merged._handler_meta[handler] = api1._handler_meta[handler]
|
|
57
|
+
|
|
58
|
+
# Merge routes from api2 (renumber handler_ids to avoid collisions)
|
|
59
|
+
for method, path, old_handler_id, handler in api2._routes:
|
|
60
|
+
new_handler_id = next_handler_id
|
|
61
|
+
next_handler_id += 1
|
|
62
|
+
merged._routes.append((method, path, new_handler_id, handler))
|
|
63
|
+
merged._handlers[new_handler_id] = handler
|
|
64
|
+
merged._handler_api_map[new_handler_id] = api2
|
|
65
|
+
if handler in api2._handler_meta:
|
|
66
|
+
merged._handler_meta[handler] = api2._handler_meta[handler]
|
|
67
|
+
|
|
68
|
+
merged._next_handler_id = next_handler_id
|
|
69
|
+
|
|
70
|
+
# Verify handler 0 (from api1) maps to api1
|
|
71
|
+
handler_id_api1 = 0
|
|
72
|
+
assert handler_id_api1 in merged._handler_api_map, \
|
|
73
|
+
f"handler_id {handler_id_api1} must exist in map"
|
|
74
|
+
|
|
75
|
+
mapped_api1 = merged._handler_api_map[handler_id_api1]
|
|
76
|
+
assert id(mapped_api1) == id(api1), \
|
|
77
|
+
f"Handler 0 must map to api1 (expected id={id(api1)}, got id={id(mapped_api1)})"
|
|
78
|
+
|
|
79
|
+
# Verify api1's logging config is preserved
|
|
80
|
+
assert mapped_api1._logging_middleware is not None, \
|
|
81
|
+
"api1 must have logging middleware"
|
|
82
|
+
assert mapped_api1._logging_middleware.config.logger_name == "api1_logger", \
|
|
83
|
+
"api1's logger name must be preserved"
|
|
84
|
+
assert mapped_api1._logging_middleware.config.request_log_fields == {"path", "client_ip"}, \
|
|
85
|
+
"api1's request log fields must be preserved"
|
|
86
|
+
|
|
87
|
+
# Verify handler 1 (from api2) maps to api2
|
|
88
|
+
handler_id_api2 = 1
|
|
89
|
+
assert handler_id_api2 in merged._handler_api_map, \
|
|
90
|
+
f"handler_id {handler_id_api2} must exist in map"
|
|
91
|
+
|
|
92
|
+
mapped_api2 = merged._handler_api_map[handler_id_api2]
|
|
93
|
+
assert id(mapped_api2) == id(api2), \
|
|
94
|
+
f"Handler 1 must map to api2 (expected id={id(api2)}, got id={id(mapped_api2)})"
|
|
95
|
+
|
|
96
|
+
# Verify api2's logging config is preserved
|
|
97
|
+
assert mapped_api2._logging_middleware is not None, \
|
|
98
|
+
"api2 must have logging middleware"
|
|
99
|
+
assert mapped_api2._logging_middleware.config.logger_name == "api2_logger", \
|
|
100
|
+
"api2's logger name must be preserved"
|
|
101
|
+
assert mapped_api2._logging_middleware.config.request_log_fields == {"method", "path"}, \
|
|
102
|
+
"api2's request log fields must be preserved"
|
|
103
|
+
|
|
104
|
+
def test_merged_api_with_different_skip_paths(self):
|
|
105
|
+
"""Each API's skip_paths should be preserved independently."""
|
|
106
|
+
# API 1 skips /health
|
|
107
|
+
config1 = LoggingConfig(skip_paths={"/health"})
|
|
108
|
+
api1 = BoltAPI(logging_config=config1)
|
|
109
|
+
|
|
110
|
+
@api1.get("/api1")
|
|
111
|
+
async def handler1():
|
|
112
|
+
return {"api": 1}
|
|
113
|
+
|
|
114
|
+
# API 2 skips /metrics
|
|
115
|
+
config2 = LoggingConfig(skip_paths={"/metrics"})
|
|
116
|
+
api2 = BoltAPI(logging_config=config2)
|
|
117
|
+
|
|
118
|
+
@api2.get("/api2")
|
|
119
|
+
async def handler2():
|
|
120
|
+
return {"api": 2}
|
|
121
|
+
|
|
122
|
+
# Merge
|
|
123
|
+
merged = BoltAPI(enable_logging=False)
|
|
124
|
+
merged._handler_api_map = {}
|
|
125
|
+
next_handler_id = 0
|
|
126
|
+
|
|
127
|
+
for method, path, old_handler_id, handler in api1._routes:
|
|
128
|
+
new_handler_id = next_handler_id
|
|
129
|
+
next_handler_id += 1
|
|
130
|
+
merged._routes.append((method, path, new_handler_id, handler))
|
|
131
|
+
merged._handlers[new_handler_id] = handler
|
|
132
|
+
merged._handler_api_map[new_handler_id] = api1
|
|
133
|
+
|
|
134
|
+
for method, path, old_handler_id, handler in api2._routes:
|
|
135
|
+
new_handler_id = next_handler_id
|
|
136
|
+
next_handler_id += 1
|
|
137
|
+
merged._routes.append((method, path, new_handler_id, handler))
|
|
138
|
+
merged._handlers[new_handler_id] = handler
|
|
139
|
+
merged._handler_api_map[new_handler_id] = api2
|
|
140
|
+
|
|
141
|
+
# Verify each API retains its own skip_paths
|
|
142
|
+
api1_from_map = merged._handler_api_map[0]
|
|
143
|
+
api2_from_map = merged._handler_api_map[1]
|
|
144
|
+
|
|
145
|
+
assert "/health" in api1_from_map._logging_middleware.config.skip_paths
|
|
146
|
+
assert "/health" not in api2_from_map._logging_middleware.config.skip_paths
|
|
147
|
+
assert "/metrics" not in api1_from_map._logging_middleware.config.skip_paths
|
|
148
|
+
assert "/metrics" in api2_from_map._logging_middleware.config.skip_paths
|
|
149
|
+
|
|
150
|
+
def test_merged_api_with_different_sample_rates(self):
|
|
151
|
+
"""Each API's sample_rate should be preserved independently."""
|
|
152
|
+
# API 1 samples at 0.05
|
|
153
|
+
config1 = LoggingConfig(sample_rate=0.05)
|
|
154
|
+
api1 = BoltAPI(logging_config=config1)
|
|
155
|
+
|
|
156
|
+
@api1.get("/api1")
|
|
157
|
+
async def handler1():
|
|
158
|
+
return {"api": 1}
|
|
159
|
+
|
|
160
|
+
# API 2 has no sampling
|
|
161
|
+
config2 = LoggingConfig(sample_rate=None)
|
|
162
|
+
api2 = BoltAPI(logging_config=config2)
|
|
163
|
+
|
|
164
|
+
@api2.get("/api2")
|
|
165
|
+
async def handler2():
|
|
166
|
+
return {"api": 2}
|
|
167
|
+
|
|
168
|
+
# Merge
|
|
169
|
+
merged = BoltAPI(enable_logging=False)
|
|
170
|
+
merged._handler_api_map = {}
|
|
171
|
+
next_handler_id = 0
|
|
172
|
+
|
|
173
|
+
for method, path, old_handler_id, handler in api1._routes:
|
|
174
|
+
new_handler_id = next_handler_id
|
|
175
|
+
next_handler_id += 1
|
|
176
|
+
merged._routes.append((method, path, new_handler_id, handler))
|
|
177
|
+
merged._handlers[new_handler_id] = handler
|
|
178
|
+
merged._handler_api_map[new_handler_id] = api1
|
|
179
|
+
|
|
180
|
+
for method, path, old_handler_id, handler in api2._routes:
|
|
181
|
+
new_handler_id = next_handler_id
|
|
182
|
+
next_handler_id += 1
|
|
183
|
+
merged._routes.append((method, path, new_handler_id, handler))
|
|
184
|
+
merged._handlers[new_handler_id] = handler
|
|
185
|
+
merged._handler_api_map[new_handler_id] = api2
|
|
186
|
+
|
|
187
|
+
# Verify each API retains its own sample_rate
|
|
188
|
+
api1_from_map = merged._handler_api_map[0]
|
|
189
|
+
api2_from_map = merged._handler_api_map[1]
|
|
190
|
+
|
|
191
|
+
assert api1_from_map._logging_middleware.config.sample_rate == 0.05
|
|
192
|
+
assert api2_from_map._logging_middleware.config.sample_rate is None
|
|
193
|
+
|
|
194
|
+
def test_merged_api_with_different_min_duration_thresholds(self):
|
|
195
|
+
"""Each API's min_duration_ms should be preserved independently."""
|
|
196
|
+
# API 1 logs only slow requests (500ms+)
|
|
197
|
+
config1 = LoggingConfig(min_duration_ms=500)
|
|
198
|
+
api1 = BoltAPI(logging_config=config1)
|
|
199
|
+
|
|
200
|
+
@api1.get("/api1")
|
|
201
|
+
async def handler1():
|
|
202
|
+
return {"api": 1}
|
|
203
|
+
|
|
204
|
+
# API 2 logs all requests (no threshold)
|
|
205
|
+
config2 = LoggingConfig(min_duration_ms=None)
|
|
206
|
+
api2 = BoltAPI(logging_config=config2)
|
|
207
|
+
|
|
208
|
+
@api2.get("/api2")
|
|
209
|
+
async def handler2():
|
|
210
|
+
return {"api": 2}
|
|
211
|
+
|
|
212
|
+
# Merge
|
|
213
|
+
merged = BoltAPI(enable_logging=False)
|
|
214
|
+
merged._handler_api_map = {}
|
|
215
|
+
next_handler_id = 0
|
|
216
|
+
|
|
217
|
+
for method, path, old_handler_id, handler in api1._routes:
|
|
218
|
+
new_handler_id = next_handler_id
|
|
219
|
+
next_handler_id += 1
|
|
220
|
+
merged._routes.append((method, path, new_handler_id, handler))
|
|
221
|
+
merged._handlers[new_handler_id] = handler
|
|
222
|
+
merged._handler_api_map[new_handler_id] = api1
|
|
223
|
+
|
|
224
|
+
for method, path, old_handler_id, handler in api2._routes:
|
|
225
|
+
new_handler_id = next_handler_id
|
|
226
|
+
next_handler_id += 1
|
|
227
|
+
merged._routes.append((method, path, new_handler_id, handler))
|
|
228
|
+
merged._handlers[new_handler_id] = handler
|
|
229
|
+
merged._handler_api_map[new_handler_id] = api2
|
|
230
|
+
|
|
231
|
+
# Verify each API retains its own min_duration_ms
|
|
232
|
+
api1_from_map = merged._handler_api_map[0]
|
|
233
|
+
api2_from_map = merged._handler_api_map[1]
|
|
234
|
+
|
|
235
|
+
assert api1_from_map._logging_middleware.config.min_duration_ms == 500
|
|
236
|
+
assert api2_from_map._logging_middleware.config.min_duration_ms is None
|
|
237
|
+
|
|
238
|
+
def test_merged_api_one_with_logging_one_without(self):
|
|
239
|
+
"""API with logging and API without logging should coexist."""
|
|
240
|
+
# API 1 with logging
|
|
241
|
+
config1 = LoggingConfig(logger_name="api1_logger")
|
|
242
|
+
api1 = BoltAPI(logging_config=config1)
|
|
243
|
+
|
|
244
|
+
@api1.get("/api1")
|
|
245
|
+
async def handler1():
|
|
246
|
+
return {"api": 1}
|
|
247
|
+
|
|
248
|
+
# API 2 without logging
|
|
249
|
+
api2 = BoltAPI(enable_logging=False)
|
|
250
|
+
|
|
251
|
+
@api2.get("/api2")
|
|
252
|
+
async def handler2():
|
|
253
|
+
return {"api": 2}
|
|
254
|
+
|
|
255
|
+
# Merge
|
|
256
|
+
merged = BoltAPI(enable_logging=False)
|
|
257
|
+
merged._handler_api_map = {}
|
|
258
|
+
next_handler_id = 0
|
|
259
|
+
|
|
260
|
+
for method, path, old_handler_id, handler in api1._routes:
|
|
261
|
+
new_handler_id = next_handler_id
|
|
262
|
+
next_handler_id += 1
|
|
263
|
+
merged._routes.append((method, path, new_handler_id, handler))
|
|
264
|
+
merged._handlers[new_handler_id] = handler
|
|
265
|
+
merged._handler_api_map[new_handler_id] = api1
|
|
266
|
+
|
|
267
|
+
for method, path, old_handler_id, handler in api2._routes:
|
|
268
|
+
new_handler_id = next_handler_id
|
|
269
|
+
next_handler_id += 1
|
|
270
|
+
merged._routes.append((method, path, new_handler_id, handler))
|
|
271
|
+
merged._handlers[new_handler_id] = handler
|
|
272
|
+
merged._handler_api_map[new_handler_id] = api2
|
|
273
|
+
|
|
274
|
+
# Verify api1 has logging, api2 doesn't
|
|
275
|
+
api1_from_map = merged._handler_api_map[0]
|
|
276
|
+
api2_from_map = merged._handler_api_map[1]
|
|
277
|
+
|
|
278
|
+
assert api1_from_map._logging_middleware is not None, \
|
|
279
|
+
"api1 must have logging middleware"
|
|
280
|
+
assert api2_from_map._logging_middleware is None, \
|
|
281
|
+
"api2 must not have logging middleware"
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class TestAPIDeduplication:
|
|
285
|
+
"""Test that duplicate API instances are properly deduplicated."""
|
|
286
|
+
|
|
287
|
+
def test_deduplication_by_object_identity(self):
|
|
288
|
+
"""Duplicate API instances (same object) should be deduplicated by id()."""
|
|
289
|
+
# Create one API
|
|
290
|
+
api1 = BoltAPI()
|
|
291
|
+
|
|
292
|
+
@api1.get("/test")
|
|
293
|
+
async def handler():
|
|
294
|
+
return {"test": True}
|
|
295
|
+
|
|
296
|
+
# Simulate autodiscovery finding the SAME api object twice
|
|
297
|
+
apis = [
|
|
298
|
+
("testproject.api:api", api1),
|
|
299
|
+
("testproject.api:api", api1), # Same object reference
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
# Deduplicate
|
|
303
|
+
seen_ids = set()
|
|
304
|
+
deduplicated = []
|
|
305
|
+
for api_path, api in apis:
|
|
306
|
+
api_id = id(api)
|
|
307
|
+
if api_id not in seen_ids:
|
|
308
|
+
seen_ids.add(api_id)
|
|
309
|
+
deduplicated.append((api_path, api))
|
|
310
|
+
|
|
311
|
+
# Should only have 1 entry
|
|
312
|
+
assert len(deduplicated) == 1, \
|
|
313
|
+
"Duplicate API instances must be deduplicated"
|
|
314
|
+
assert deduplicated[0] == ("testproject.api:api", api1)
|
|
315
|
+
|
|
316
|
+
def test_different_instances_are_not_deduplicated(self):
|
|
317
|
+
"""Different API instances should NOT be deduplicated."""
|
|
318
|
+
# Create two different APIs
|
|
319
|
+
api1 = BoltAPI()
|
|
320
|
+
|
|
321
|
+
@api1.get("/api1")
|
|
322
|
+
async def handler1():
|
|
323
|
+
return {"api": 1}
|
|
324
|
+
|
|
325
|
+
api2 = BoltAPI()
|
|
326
|
+
|
|
327
|
+
@api2.get("/api2")
|
|
328
|
+
async def handler2():
|
|
329
|
+
return {"api": 2}
|
|
330
|
+
|
|
331
|
+
# Both should be kept (different objects)
|
|
332
|
+
apis = [
|
|
333
|
+
("app1.api:api", api1),
|
|
334
|
+
("app2.api:api", api2),
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
# Deduplicate
|
|
338
|
+
seen_ids = set()
|
|
339
|
+
deduplicated = []
|
|
340
|
+
for api_path, api in apis:
|
|
341
|
+
api_id = id(api)
|
|
342
|
+
if api_id not in seen_ids:
|
|
343
|
+
seen_ids.add(api_id)
|
|
344
|
+
deduplicated.append((api_path, api))
|
|
345
|
+
|
|
346
|
+
# Should have both entries (different objects)
|
|
347
|
+
assert len(deduplicated) == 2, \
|
|
348
|
+
"Different API instances must not be deduplicated"
|
|
349
|
+
assert id(deduplicated[0][1]) != id(deduplicated[1][1])
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class TestLoggingWithHandlerCalls:
|
|
353
|
+
"""Test that logging actually works when handlers are called."""
|
|
354
|
+
|
|
355
|
+
def test_logging_middleware_logs_on_handler_call(self, caplog):
|
|
356
|
+
"""Logging middleware should log when handler is invoked."""
|
|
357
|
+
config = LoggingConfig(logger_name="test.api", min_duration_ms=None)
|
|
358
|
+
api = BoltAPI(logging_config=config)
|
|
359
|
+
|
|
360
|
+
@api.get("/test")
|
|
361
|
+
async def test_handler():
|
|
362
|
+
return {"result": "success"}
|
|
363
|
+
|
|
364
|
+
# Simulate calling the handler (simplified)
|
|
365
|
+
request = {
|
|
366
|
+
"method": "GET",
|
|
367
|
+
"path": "/test",
|
|
368
|
+
"query_params": {},
|
|
369
|
+
"headers": {},
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
# Log the response manually (simulating what BoltAPI.call_handler does)
|
|
373
|
+
with caplog.at_level(logging.INFO, logger="test.api"):
|
|
374
|
+
api._logging_middleware.log_response(request, 200, 0.1)
|
|
375
|
+
|
|
376
|
+
# Should have logged
|
|
377
|
+
assert len(caplog.records) > 0, "Handler response should be logged"
|
|
378
|
+
assert caplog.records[0].status_code == 200
|
|
379
|
+
|
|
380
|
+
def test_logging_middleware_logs_exceptions(self, caplog):
|
|
381
|
+
"""Logging middleware should log exceptions."""
|
|
382
|
+
config = LoggingConfig(logger_name="test.api")
|
|
383
|
+
api = BoltAPI(logging_config=config)
|
|
384
|
+
|
|
385
|
+
@api.get("/error")
|
|
386
|
+
async def error_handler():
|
|
387
|
+
raise ValueError("Test error")
|
|
388
|
+
|
|
389
|
+
# Simulate exception logging
|
|
390
|
+
request = {
|
|
391
|
+
"method": "GET",
|
|
392
|
+
"path": "/error",
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
exc = ValueError("Test error")
|
|
396
|
+
|
|
397
|
+
with caplog.at_level(logging.ERROR, logger="test.api"):
|
|
398
|
+
api._logging_middleware.log_exception(request, exc, exc_info=False)
|
|
399
|
+
|
|
400
|
+
# Should have logged exception
|
|
401
|
+
assert len(caplog.records) > 0, "Exception should be logged"
|
|
402
|
+
assert "ValueError" in caplog.records[0].message
|
|
403
|
+
assert "Test error" in caplog.records[0].message
|
|
404
|
+
|
|
405
|
+
def test_disabled_logging_does_not_log(self, caplog):
|
|
406
|
+
"""API with logging disabled should not log."""
|
|
407
|
+
api = BoltAPI(enable_logging=False)
|
|
408
|
+
|
|
409
|
+
@api.get("/test")
|
|
410
|
+
async def test_handler():
|
|
411
|
+
return {"result": "success"}
|
|
412
|
+
|
|
413
|
+
# Verify no logging middleware
|
|
414
|
+
assert api._logging_middleware is None, \
|
|
415
|
+
"API with enable_logging=False must not have logging middleware"
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
if __name__ == "__main__":
|
|
419
|
+
pytest.main([__file__, "-v", "-s"])
|