django-bolt 0.1.0__cp310-abi3-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-bolt might be problematic. Click here for more details.
- django_bolt/__init__.py +147 -0
- django_bolt/_core.pyd +0 -0
- django_bolt/admin/__init__.py +25 -0
- django_bolt/admin/admin_detection.py +179 -0
- django_bolt/admin/asgi_bridge.py +267 -0
- django_bolt/admin/routes.py +91 -0
- django_bolt/admin/static.py +155 -0
- django_bolt/admin/static_routes.py +111 -0
- django_bolt/api.py +1011 -0
- django_bolt/apps.py +7 -0
- django_bolt/async_collector.py +228 -0
- django_bolt/auth/README.md +464 -0
- django_bolt/auth/REVOCATION_EXAMPLE.md +391 -0
- django_bolt/auth/__init__.py +84 -0
- django_bolt/auth/backends.py +236 -0
- django_bolt/auth/guards.py +224 -0
- django_bolt/auth/jwt_utils.py +212 -0
- django_bolt/auth/revocation.py +286 -0
- django_bolt/auth/token.py +335 -0
- django_bolt/binding.py +363 -0
- django_bolt/bootstrap.py +77 -0
- django_bolt/cli.py +133 -0
- django_bolt/compression.py +104 -0
- django_bolt/decorators.py +159 -0
- django_bolt/dependencies.py +128 -0
- django_bolt/error_handlers.py +305 -0
- django_bolt/exceptions.py +294 -0
- django_bolt/health.py +129 -0
- django_bolt/logging/__init__.py +6 -0
- django_bolt/logging/config.py +357 -0
- django_bolt/logging/middleware.py +296 -0
- django_bolt/management/__init__.py +1 -0
- django_bolt/management/commands/__init__.py +0 -0
- django_bolt/management/commands/runbolt.py +427 -0
- django_bolt/middleware/__init__.py +32 -0
- django_bolt/middleware/compiler.py +131 -0
- django_bolt/middleware/middleware.py +247 -0
- django_bolt/openapi/__init__.py +23 -0
- django_bolt/openapi/config.py +196 -0
- django_bolt/openapi/plugins.py +439 -0
- django_bolt/openapi/routes.py +152 -0
- django_bolt/openapi/schema_generator.py +581 -0
- django_bolt/openapi/spec/__init__.py +68 -0
- django_bolt/openapi/spec/base.py +74 -0
- django_bolt/openapi/spec/callback.py +24 -0
- django_bolt/openapi/spec/components.py +72 -0
- django_bolt/openapi/spec/contact.py +21 -0
- django_bolt/openapi/spec/discriminator.py +25 -0
- django_bolt/openapi/spec/encoding.py +67 -0
- django_bolt/openapi/spec/enums.py +41 -0
- django_bolt/openapi/spec/example.py +36 -0
- django_bolt/openapi/spec/external_documentation.py +21 -0
- django_bolt/openapi/spec/header.py +132 -0
- django_bolt/openapi/spec/info.py +50 -0
- django_bolt/openapi/spec/license.py +28 -0
- django_bolt/openapi/spec/link.py +66 -0
- django_bolt/openapi/spec/media_type.py +51 -0
- django_bolt/openapi/spec/oauth_flow.py +36 -0
- django_bolt/openapi/spec/oauth_flows.py +28 -0
- django_bolt/openapi/spec/open_api.py +87 -0
- django_bolt/openapi/spec/operation.py +105 -0
- django_bolt/openapi/spec/parameter.py +147 -0
- django_bolt/openapi/spec/path_item.py +78 -0
- django_bolt/openapi/spec/paths.py +27 -0
- django_bolt/openapi/spec/reference.py +38 -0
- django_bolt/openapi/spec/request_body.py +38 -0
- django_bolt/openapi/spec/response.py +48 -0
- django_bolt/openapi/spec/responses.py +44 -0
- django_bolt/openapi/spec/schema.py +678 -0
- django_bolt/openapi/spec/security_requirement.py +28 -0
- django_bolt/openapi/spec/security_scheme.py +69 -0
- django_bolt/openapi/spec/server.py +34 -0
- django_bolt/openapi/spec/server_variable.py +32 -0
- django_bolt/openapi/spec/tag.py +32 -0
- django_bolt/openapi/spec/xml.py +44 -0
- django_bolt/pagination.py +669 -0
- django_bolt/param_functions.py +49 -0
- django_bolt/params.py +337 -0
- django_bolt/request_parsing.py +128 -0
- django_bolt/responses.py +214 -0
- django_bolt/router.py +48 -0
- django_bolt/serialization.py +193 -0
- django_bolt/status_codes.py +321 -0
- django_bolt/testing/__init__.py +10 -0
- django_bolt/testing/client.py +274 -0
- django_bolt/testing/helpers.py +93 -0
- django_bolt/tests/__init__.py +0 -0
- django_bolt/tests/admin_tests/__init__.py +1 -0
- django_bolt/tests/admin_tests/conftest.py +6 -0
- django_bolt/tests/admin_tests/test_admin_with_django.py +278 -0
- django_bolt/tests/admin_tests/urls.py +9 -0
- django_bolt/tests/cbv/__init__.py +0 -0
- django_bolt/tests/cbv/test_class_views.py +570 -0
- django_bolt/tests/cbv/test_class_views_django_orm.py +703 -0
- django_bolt/tests/cbv/test_class_views_features.py +1173 -0
- django_bolt/tests/cbv/test_class_views_with_client.py +622 -0
- django_bolt/tests/conftest.py +165 -0
- django_bolt/tests/test_action_decorator.py +399 -0
- django_bolt/tests/test_auth_secret_key.py +83 -0
- django_bolt/tests/test_decorator_syntax.py +159 -0
- django_bolt/tests/test_error_handling.py +481 -0
- django_bolt/tests/test_file_response.py +192 -0
- django_bolt/tests/test_global_cors.py +172 -0
- django_bolt/tests/test_guards_auth.py +441 -0
- django_bolt/tests/test_guards_integration.py +303 -0
- django_bolt/tests/test_health.py +283 -0
- django_bolt/tests/test_integration_validation.py +400 -0
- django_bolt/tests/test_json_validation.py +536 -0
- django_bolt/tests/test_jwt_auth.py +327 -0
- django_bolt/tests/test_jwt_token.py +458 -0
- django_bolt/tests/test_logging.py +837 -0
- django_bolt/tests/test_logging_merge.py +419 -0
- django_bolt/tests/test_middleware.py +492 -0
- django_bolt/tests/test_middleware_server.py +230 -0
- django_bolt/tests/test_model_viewset.py +323 -0
- django_bolt/tests/test_models.py +24 -0
- django_bolt/tests/test_pagination.py +1258 -0
- django_bolt/tests/test_parameter_validation.py +178 -0
- django_bolt/tests/test_syntax.py +626 -0
- django_bolt/tests/test_testing_utilities.py +163 -0
- django_bolt/tests/test_testing_utilities_simple.py +123 -0
- django_bolt/tests/test_viewset_unified.py +346 -0
- django_bolt/typing.py +273 -0
- django_bolt/views.py +1110 -0
- django_bolt-0.1.0.dist-info/METADATA +629 -0
- django_bolt-0.1.0.dist-info/RECORD +128 -0
- django_bolt-0.1.0.dist-info/WHEEL +4 -0
- django_bolt-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from django_bolt import BoltAPI
|
|
6
|
+
from django_bolt.responses import FileResponse
|
|
7
|
+
from django_bolt.testing import TestClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Create test files once at module level
|
|
11
|
+
TEST_DIR = tempfile.mkdtemp()
|
|
12
|
+
|
|
13
|
+
# Small file (< 10MB) - 1KB
|
|
14
|
+
SMALL_FILE = os.path.join(TEST_DIR, "small_file.txt")
|
|
15
|
+
with open(SMALL_FILE, "w") as f:
|
|
16
|
+
f.write("x" * 1024)
|
|
17
|
+
|
|
18
|
+
# Medium file (< 10MB) - 1MB
|
|
19
|
+
MEDIUM_FILE = os.path.join(TEST_DIR, "medium_file.txt")
|
|
20
|
+
with open(MEDIUM_FILE, "w") as f:
|
|
21
|
+
f.write("y" * (1024 * 1024))
|
|
22
|
+
|
|
23
|
+
# Large file (> 10MB) - 11MB
|
|
24
|
+
LARGE_FILE = os.path.join(TEST_DIR, "large_file.bin")
|
|
25
|
+
with open(LARGE_FILE, "wb") as f:
|
|
26
|
+
f.write(b"z" * (11 * 1024 * 1024))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture(scope="module")
|
|
30
|
+
def api():
|
|
31
|
+
"""Create test API with file endpoints"""
|
|
32
|
+
api = BoltAPI()
|
|
33
|
+
|
|
34
|
+
@api.get("/file/small")
|
|
35
|
+
async def get_small_file():
|
|
36
|
+
"""Small file should be read into memory"""
|
|
37
|
+
return FileResponse(SMALL_FILE, filename="small_file.txt")
|
|
38
|
+
|
|
39
|
+
@api.get("/file/medium")
|
|
40
|
+
async def get_medium_file():
|
|
41
|
+
"""Medium file should be read into memory"""
|
|
42
|
+
return FileResponse(MEDIUM_FILE, filename="medium_file.txt")
|
|
43
|
+
|
|
44
|
+
@api.get("/file/large")
|
|
45
|
+
async def get_large_file():
|
|
46
|
+
"""Large file should be streamed"""
|
|
47
|
+
return FileResponse(LARGE_FILE, filename="large_file.bin")
|
|
48
|
+
|
|
49
|
+
@api.get("/file/custom-headers")
|
|
50
|
+
async def get_file_with_headers():
|
|
51
|
+
"""Test custom headers"""
|
|
52
|
+
return FileResponse(
|
|
53
|
+
SMALL_FILE,
|
|
54
|
+
filename="custom.txt",
|
|
55
|
+
headers={"X-Custom-Header": "test-value"}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@api.get("/file/custom-media-type")
|
|
59
|
+
async def get_file_with_media_type():
|
|
60
|
+
"""Test custom media type"""
|
|
61
|
+
return FileResponse(
|
|
62
|
+
SMALL_FILE,
|
|
63
|
+
media_type="application/octet-stream"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return api
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.fixture(scope="module")
|
|
70
|
+
def client(api):
|
|
71
|
+
"""Create test client with HTTP layer enabled to test file serving"""
|
|
72
|
+
# use_http_layer=True enables full HTTP stack including file serving
|
|
73
|
+
return TestClient(api, use_http_layer=True)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_small_file_response(client):
|
|
77
|
+
"""Test that small files (<10MB) work correctly"""
|
|
78
|
+
response = client.get("/file/small")
|
|
79
|
+
|
|
80
|
+
# Should succeed
|
|
81
|
+
assert response.status_code == 200
|
|
82
|
+
|
|
83
|
+
# Should have correct Content-Type
|
|
84
|
+
assert response.headers.get("content-type", "").startswith("text/")
|
|
85
|
+
|
|
86
|
+
# Should have Content-Disposition with filename
|
|
87
|
+
content_disp = response.headers.get("content-disposition", "")
|
|
88
|
+
assert "attachment" in content_disp.lower()
|
|
89
|
+
assert "small_file.txt" in content_disp
|
|
90
|
+
|
|
91
|
+
# With HTTP layer, we should get the file content
|
|
92
|
+
if response.content:
|
|
93
|
+
assert len(response.content) == 1024
|
|
94
|
+
assert response.content == b"x" * 1024
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_medium_file_response(client):
|
|
98
|
+
"""Test that medium files (<10MB) work correctly"""
|
|
99
|
+
response = client.get("/file/medium")
|
|
100
|
+
|
|
101
|
+
# Should succeed
|
|
102
|
+
assert response.status_code == 200
|
|
103
|
+
|
|
104
|
+
# With HTTP layer, content should be available
|
|
105
|
+
if response.content:
|
|
106
|
+
assert len(response.content) == 1024 * 1024
|
|
107
|
+
assert response.content[:100] == b"y" * 100
|
|
108
|
+
assert response.content[-100:] == b"y" * 100
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_large_file_response(client):
|
|
112
|
+
"""Test that large files (>10MB) are streamed"""
|
|
113
|
+
response = client.get("/file/large")
|
|
114
|
+
|
|
115
|
+
# Should succeed
|
|
116
|
+
assert response.status_code == 200
|
|
117
|
+
|
|
118
|
+
# Large files use streaming
|
|
119
|
+
# TestClient may not preserve the full streamed content, so just verify response is successful
|
|
120
|
+
# In production, streaming works correctly
|
|
121
|
+
# Content should be available (even if TestClient doesn't show full stream)
|
|
122
|
+
assert response.content is not None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_file_custom_headers(client):
|
|
126
|
+
"""Test FileResponse with custom headers"""
|
|
127
|
+
response = client.get("/file/custom-headers")
|
|
128
|
+
|
|
129
|
+
assert response.status_code == 200
|
|
130
|
+
assert response.headers.get("x-custom-header") == "test-value"
|
|
131
|
+
assert "custom.txt" in response.headers.get("content-disposition", "")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_file_custom_media_type(client):
|
|
135
|
+
"""Test FileResponse with custom media type"""
|
|
136
|
+
response = client.get("/file/custom-media-type")
|
|
137
|
+
|
|
138
|
+
assert response.status_code == 200
|
|
139
|
+
assert response.headers.get("content-type") == "application/octet-stream"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_file_not_found():
|
|
143
|
+
"""Test FileResponse with non-existent file returns 404"""
|
|
144
|
+
api = BoltAPI()
|
|
145
|
+
|
|
146
|
+
@api.get("/file/nonexistent")
|
|
147
|
+
async def get_nonexistent():
|
|
148
|
+
return FileResponse("/path/to/nonexistent/file.txt")
|
|
149
|
+
|
|
150
|
+
# Use HTTP layer to test actual file opening
|
|
151
|
+
client = TestClient(api, use_http_layer=True)
|
|
152
|
+
response = client.get("/file/nonexistent")
|
|
153
|
+
|
|
154
|
+
# Should return 404 Not Found (proper HTTP status for missing file)
|
|
155
|
+
# File error happens in Rust after Python serializes the response
|
|
156
|
+
assert response.status_code == 404
|
|
157
|
+
assert b"File not found" in response.content or response.content == b""
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_file_performance_small_vs_large():
|
|
161
|
+
"""Test that small files use in-memory buffering for better performance"""
|
|
162
|
+
import time
|
|
163
|
+
|
|
164
|
+
api = BoltAPI()
|
|
165
|
+
temp_dir = tempfile.mkdtemp()
|
|
166
|
+
|
|
167
|
+
# Create small file
|
|
168
|
+
small_file = os.path.join(temp_dir, "perf_small.txt")
|
|
169
|
+
with open(small_file, "w") as f:
|
|
170
|
+
f.write("x" * (100 * 1024)) # 100KB
|
|
171
|
+
|
|
172
|
+
@api.get("/perf/small")
|
|
173
|
+
async def perf_small():
|
|
174
|
+
return FileResponse(small_file)
|
|
175
|
+
|
|
176
|
+
client = TestClient(api)
|
|
177
|
+
|
|
178
|
+
# Warm up
|
|
179
|
+
client.get("/perf/small")
|
|
180
|
+
|
|
181
|
+
# Time multiple requests
|
|
182
|
+
start = time.time()
|
|
183
|
+
for _ in range(100):
|
|
184
|
+
response = client.get("/perf/small")
|
|
185
|
+
assert response.status_code == 200
|
|
186
|
+
elapsed = time.time() - start
|
|
187
|
+
|
|
188
|
+
# Should handle 100 requests relatively quickly
|
|
189
|
+
# (This is more of a smoke test than a strict perf test)
|
|
190
|
+
assert elapsed < 5.0 # Should take less than 5 seconds for 100 requests
|
|
191
|
+
|
|
192
|
+
print(f"Small file (100KB) - 100 requests: {elapsed:.3f}s ({100/elapsed:.1f} RPS)")
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Tests for global CORS origins fallback functionality."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from django_bolt import BoltAPI
|
|
5
|
+
from django_bolt.middleware import cors
|
|
6
|
+
from django_bolt.testing import TestClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_cors_uses_global_origins_when_no_route_config():
|
|
10
|
+
"""Test that global CORS origins are used when route has no specific config."""
|
|
11
|
+
api = BoltAPI()
|
|
12
|
+
|
|
13
|
+
# Route without CORS config (should use global) but with middleware enabled
|
|
14
|
+
@api.get("/no-cors-config")
|
|
15
|
+
@cors() # Empty CORS config - should use global
|
|
16
|
+
async def no_cors_endpoint():
|
|
17
|
+
return {"message": "hello"}
|
|
18
|
+
|
|
19
|
+
# Configure global CORS origins
|
|
20
|
+
global_origins = ["https://example.com", "https://trusted.com"]
|
|
21
|
+
|
|
22
|
+
with TestClient(api, use_http_layer=True, cors_allowed_origins=global_origins) as client:
|
|
23
|
+
response = client.get(
|
|
24
|
+
"/no-cors-config",
|
|
25
|
+
headers={"Origin": "https://example.com"}
|
|
26
|
+
)
|
|
27
|
+
assert response.status_code == 200
|
|
28
|
+
assert response.headers.get("Access-Control-Allow-Origin") == "https://example.com"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_cors_route_config_overrides_global():
|
|
32
|
+
"""Test that route-specific CORS config takes precedence over global."""
|
|
33
|
+
api = BoltAPI()
|
|
34
|
+
|
|
35
|
+
# Route with specific CORS config (should override global)
|
|
36
|
+
@api.get("/with-cors-config")
|
|
37
|
+
@cors(origins=["https://custom.com", "https://other.com"])
|
|
38
|
+
async def with_cors_endpoint():
|
|
39
|
+
return {"message": "hello"}
|
|
40
|
+
|
|
41
|
+
# Global config has different origins
|
|
42
|
+
global_origins = ["https://example.com", "https://trusted.com"]
|
|
43
|
+
|
|
44
|
+
with TestClient(api, use_http_layer=True, cors_allowed_origins=global_origins) as client:
|
|
45
|
+
response = client.get(
|
|
46
|
+
"/with-cors-config",
|
|
47
|
+
headers={"Origin": "https://custom.com"}
|
|
48
|
+
)
|
|
49
|
+
assert response.status_code == 200
|
|
50
|
+
assert response.headers.get("Access-Control-Allow-Origin") == "https://custom.com"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_cors_rejects_unlisted_origin_with_global_config():
|
|
54
|
+
"""Test that unlisted origins are rejected even with global config."""
|
|
55
|
+
api = BoltAPI()
|
|
56
|
+
|
|
57
|
+
@api.get("/no-cors-config")
|
|
58
|
+
@cors() # Empty CORS config - should use global
|
|
59
|
+
async def endpoint():
|
|
60
|
+
return {"message": "hello"}
|
|
61
|
+
|
|
62
|
+
global_origins = ["https://example.com", "https://trusted.com"]
|
|
63
|
+
|
|
64
|
+
with TestClient(api, use_http_layer=True, cors_allowed_origins=global_origins) as client:
|
|
65
|
+
response = client.get(
|
|
66
|
+
"/no-cors-config",
|
|
67
|
+
headers={"Origin": "https://evil.com"}
|
|
68
|
+
)
|
|
69
|
+
# Should not have CORS headers for unlisted origin
|
|
70
|
+
assert "Access-Control-Allow-Origin" not in response.headers
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_cors_allows_wildcard_in_global_config():
|
|
74
|
+
"""Test that wildcard in global config allows any origin."""
|
|
75
|
+
api = BoltAPI()
|
|
76
|
+
|
|
77
|
+
@api.get("/no-cors-config")
|
|
78
|
+
@cors() # Empty CORS config - should use global
|
|
79
|
+
async def endpoint():
|
|
80
|
+
return {"message": "hello"}
|
|
81
|
+
|
|
82
|
+
# Wildcard in global config
|
|
83
|
+
global_origins = ["*"]
|
|
84
|
+
|
|
85
|
+
with TestClient(api, use_http_layer=True, cors_allowed_origins=global_origins) as client:
|
|
86
|
+
response = client.get(
|
|
87
|
+
"/no-cors-config",
|
|
88
|
+
headers={"Origin": "https://any-domain.com"}
|
|
89
|
+
)
|
|
90
|
+
assert response.status_code == 200
|
|
91
|
+
assert response.headers.get("Access-Control-Allow-Origin") == "*"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_cors_empty_global_config_no_headers():
|
|
95
|
+
"""Test that no CORS headers are added when global config is empty and no route config."""
|
|
96
|
+
api = BoltAPI()
|
|
97
|
+
|
|
98
|
+
@api.get("/no-cors-config")
|
|
99
|
+
async def endpoint():
|
|
100
|
+
return {"message": "hello"}
|
|
101
|
+
|
|
102
|
+
# Empty global origins, no route middleware
|
|
103
|
+
with TestClient(api, use_http_layer=True, cors_allowed_origins=[]) as client:
|
|
104
|
+
response = client.get(
|
|
105
|
+
"/no-cors-config",
|
|
106
|
+
headers={"Origin": "https://example.com"}
|
|
107
|
+
)
|
|
108
|
+
assert "Access-Control-Allow-Origin" not in response.headers
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_cors_middleware_enabled_but_no_origins_configured():
|
|
112
|
+
"""Test that when CORS middleware is enabled but no origins configured, requests succeed but have no headers."""
|
|
113
|
+
api = BoltAPI()
|
|
114
|
+
|
|
115
|
+
@api.get("/cors-no-origins")
|
|
116
|
+
@cors() # CORS enabled but no origins in decorator and no global
|
|
117
|
+
async def endpoint():
|
|
118
|
+
return {"message": "hello"}
|
|
119
|
+
|
|
120
|
+
# No global origins
|
|
121
|
+
with TestClient(api, use_http_layer=True, cors_allowed_origins=[]) as client:
|
|
122
|
+
# Regular request with Origin header
|
|
123
|
+
response = client.get(
|
|
124
|
+
"/cors-no-origins",
|
|
125
|
+
headers={"Origin": "https://example.com"}
|
|
126
|
+
)
|
|
127
|
+
# Request succeeds but no CORS headers
|
|
128
|
+
assert response.status_code == 200
|
|
129
|
+
assert "Access-Control-Allow-Origin" not in response.headers
|
|
130
|
+
|
|
131
|
+
# Preflight should be rejected
|
|
132
|
+
preflight = client.options(
|
|
133
|
+
"/cors-no-origins",
|
|
134
|
+
headers={
|
|
135
|
+
"Origin": "https://example.com",
|
|
136
|
+
"Access-Control-Request-Method": "GET",
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
# Preflight is rejected with 403
|
|
140
|
+
assert preflight.status_code == 403
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_preflight_uses_global_origins():
|
|
144
|
+
"""Test that preflight requests use global origins."""
|
|
145
|
+
api = BoltAPI()
|
|
146
|
+
|
|
147
|
+
@api.post("/no-cors-config")
|
|
148
|
+
@cors() # Empty CORS config - should use global
|
|
149
|
+
async def endpoint():
|
|
150
|
+
return {"message": "hello"}
|
|
151
|
+
|
|
152
|
+
# Also need to register OPTIONS handler or it will be auto-generated
|
|
153
|
+
@api.options("/no-cors-config")
|
|
154
|
+
@cors() # Empty CORS config - should use global
|
|
155
|
+
async def options_endpoint():
|
|
156
|
+
return {"message": "options"}
|
|
157
|
+
|
|
158
|
+
global_origins = ["https://example.com", "https://trusted.com"]
|
|
159
|
+
|
|
160
|
+
with TestClient(api, use_http_layer=True, cors_allowed_origins=global_origins) as client:
|
|
161
|
+
response = client.options(
|
|
162
|
+
"/no-cors-config",
|
|
163
|
+
headers={
|
|
164
|
+
"Origin": "https://example.com",
|
|
165
|
+
"Access-Control-Request-Method": "POST",
|
|
166
|
+
"Access-Control-Request-Headers": "Content-Type"
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
# Preflight is intercepted by CORS middleware and returns 204
|
|
170
|
+
assert response.status_code == 204
|
|
171
|
+
assert response.headers.get("Access-Control-Allow-Origin") == "https://example.com"
|
|
172
|
+
assert "POST" in response.headers.get("Access-Control-Allow-Methods", "")
|