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
67 lines
2.5 KiB
Python
67 lines
2.5 KiB
Python
"""
|
|
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
|