c2-core: add CORS middleware so the browser can call the REST API (#110)
The Archive page's GET /calls/search failed its CORS preflight (OPTIONS -> 405, no Access-Control-* headers). Allow the app origin(s) explicitly for the standard methods and the authorization/content-type headers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tbknwttzou4s46PAykmtix
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
bccb3e0316
commit
d60fef67ad
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
End-to-end CORS wiring for the one browser-facing REST surface.
|
||||
|
||||
The frontend's Archive page calls GET /calls/search with Authorization +
|
||||
Content-Type headers, which forces the browser to send a CORS preflight
|
||||
first. Before #110 that OPTIONS got a bare 405 with no Access-Control-*
|
||||
headers and the fetch failed with "TypeError: Failed to fetch". These
|
||||
tests drive the real app through TestClient so a regression in the
|
||||
middleware wiring (not just the helper) is caught.
|
||||
|
||||
TestClient is NOT used as a context manager on purpose: that would run the
|
||||
lifespan (mqtt_handler.connect(), the sweeper loops, dynsec bootstrap),
|
||||
none of which is needed here -- CORSMiddleware answers a preflight before
|
||||
routing or dependencies run.
|
||||
"""
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.config import settings
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
ALLOWED_ORIGIN = "https://drb.cusano.net"
|
||||
DISALLOWED_ORIGIN = "https://evil.example.com"
|
||||
|
||||
|
||||
def test_default_allowed_origin_matches_the_deployed_frontend():
|
||||
# The frontend is served on the bare domain (infra Caddyfile.j2), so the
|
||||
# default must allow exactly that origin without any env override.
|
||||
assert ALLOWED_ORIGIN in settings.cors_origins
|
||||
|
||||
|
||||
def test_preflight_for_calls_search_is_allowed():
|
||||
resp = client.options(
|
||||
"/calls/search",
|
||||
headers={
|
||||
"Origin": ALLOWED_ORIGIN,
|
||||
"Access-Control-Request-Method": "GET",
|
||||
"Access-Control-Request-Headers": "authorization,content-type",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.headers.get("access-control-allow-origin") == ALLOWED_ORIGIN
|
||||
allow_methods = resp.headers.get("access-control-allow-methods", "").upper()
|
||||
assert "GET" in allow_methods
|
||||
# Bearer auth, not cookies -- credentials must never be advertised.
|
||||
assert "access-control-allow-credentials" not in resp.headers
|
||||
|
||||
|
||||
def test_preflight_from_disallowed_origin_gets_no_allow_origin():
|
||||
resp = client.options(
|
||||
"/calls/search",
|
||||
headers={
|
||||
"Origin": DISALLOWED_ORIGIN,
|
||||
"Access-Control-Request-Method": "GET",
|
||||
},
|
||||
)
|
||||
assert resp.headers.get("access-control-allow-origin") is None
|
||||
|
||||
|
||||
def test_simple_get_from_allowed_origin_is_annotated():
|
||||
# Even a non-preflight GET must carry Access-Control-Allow-Origin or the
|
||||
# browser hides the response body from the page.
|
||||
resp = client.get("/health", headers={"Origin": ALLOWED_ORIGIN})
|
||||
assert resp.status_code == 200
|
||||
assert resp.headers.get("access-control-allow-origin") == ALLOWED_ORIGIN
|
||||
Reference in New Issue
Block a user