Health Score updation
This commit is contained in:
@@ -27,7 +27,13 @@ PORTS=3000,8000
|
||||
# Both spellings are listed so this keeps working if the typo is ever corrected
|
||||
# in Dokploy. Exact origins, never a wildcard: the app sends an Authorization
|
||||
# header, and browsers reject credentialed requests to a wildcard origin.
|
||||
API_CORS_ORIGINS=https://catalouge.nearle.ai.in,https://catalogue.nearle.ai.in
|
||||
#
|
||||
# app.nearledaily.com is the merchant console, which calls /api/nutrition/*
|
||||
# from the browser. Until it was listed here every one of those calls failed
|
||||
# as an opaque "Failed to fetch" while curl returned 200 - the server saw a
|
||||
# healthy request and the browser discarded the response. localhost:3100 is
|
||||
# that console in local development.
|
||||
API_CORS_ORIGINS=https://catalouge.nearle.ai.in,https://catalogue.nearle.ai.in,https://app.nearledaily.com,http://localhost:3100
|
||||
|
||||
# --- Authentication --------------------------------------------------------
|
||||
AUTH_ENABLED=true
|
||||
|
||||
129
tests/test_cors_origins.py
Normal file
129
tests/test_cors_origins.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""The browser is the only client that enforces CORS, and no test here is one.
|
||||
|
||||
THE FAILURE THIS FILE EXISTS FOR
|
||||
--------------------------------
|
||||
The merchant console called `https://mcp.nearle.ai.in/api/...` from Chrome and got
|
||||
`TypeError: Failed to fetch` for a whole day. The same URL under curl returned 200
|
||||
with the right body, so every server-side check - ours and theirs - passed. The
|
||||
response carried `Access-Control-Allow-Credentials: true` and no
|
||||
`Access-Control-Allow-Origin`, and the browser discarded it before any code saw it.
|
||||
|
||||
The cause was not missing middleware. `CORSMiddleware` was installed and correct;
|
||||
the console's origin was simply not in `API_CORS_ORIGINS`, so Starlette declined to
|
||||
echo the header. Nothing in the server log looked wrong: a healthy 200, every time.
|
||||
|
||||
That is why the deployed allowlist is asserted as DATA below. A unit test cannot
|
||||
fail the way a browser fails, so the next best thing is to pin the one value whose
|
||||
absence produces a silent, total outage for a first-party client.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ENV_PRODUCTION = Path(__file__).resolve().parents[1] / ".env.production"
|
||||
|
||||
# Every first-party browser client of this API. A new one added to the console
|
||||
# and not added here is the bug above, repeated.
|
||||
REQUIRED_ORIGINS = (
|
||||
"https://app.nearledaily.com", # merchant console
|
||||
"http://localhost:3100", # merchant console, local development
|
||||
"https://catalogue.nearle.ai.in", # catalogue frontend
|
||||
)
|
||||
|
||||
|
||||
def _deployed_origins():
|
||||
"""The allowlist the image ships with.
|
||||
|
||||
Read from the file rather than from `settings`, because the test process has
|
||||
its own environment - importing the setting would assert on the developer's
|
||||
machine instead of on what gets deployed.
|
||||
"""
|
||||
if not ENV_PRODUCTION.exists():
|
||||
pytest.skip(".env.production is not present in this checkout")
|
||||
for line in ENV_PRODUCTION.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("API_CORS_ORIGINS="):
|
||||
value = line.split("=", 1)[1]
|
||||
return [o.strip() for o in value.split(",") if o.strip()]
|
||||
return []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("origin", REQUIRED_ORIGINS)
|
||||
def test_every_first_party_browser_client_is_allowed(origin):
|
||||
assert origin in _deployed_origins(), (
|
||||
"%s is missing from API_CORS_ORIGINS in .env.production. Browser calls "
|
||||
"from it fail as an opaque 'Failed to fetch' while curl still returns "
|
||||
"200, so nothing server-side will catch this." % origin
|
||||
)
|
||||
|
||||
|
||||
def test_the_allowlist_is_not_a_wildcard():
|
||||
"""A wildcard would disable `allow_credentials` (see the guard in main.py),
|
||||
silently breaking any authenticated browser call to this API."""
|
||||
assert "*" not in _deployed_origins()
|
||||
|
||||
|
||||
def test_every_origin_is_a_bare_scheme_and_host():
|
||||
"""An Origin header is scheme + host + port, never a path and never a
|
||||
trailing slash. `https://app.nearledaily.com/` does not match and fails
|
||||
exactly as if it were absent."""
|
||||
for origin in _deployed_origins():
|
||||
assert re.fullmatch(r"https?://[A-Za-z0-9.\-]+(:\d+)?", origin), origin
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The middleware itself
|
||||
# ---------------------------------------------------------------------------
|
||||
# These run against whatever origins the TEST environment carries, so they pin
|
||||
# the behaviour rather than the deployed list: an allowed origin is echoed, an
|
||||
# unknown one is not, and a preflight from an unknown origin is refused.
|
||||
|
||||
def _allowed_origin():
|
||||
from app.infrastructure.settings import API_CORS_ORIGINS
|
||||
if not API_CORS_ORIGINS:
|
||||
pytest.skip("no CORS origins configured in the test environment")
|
||||
return API_CORS_ORIGINS[0]
|
||||
|
||||
|
||||
def test_an_allowed_origin_is_echoed_back(client):
|
||||
origin = _allowed_origin()
|
||||
|
||||
response = client.get("/api/health", headers={"Origin": origin})
|
||||
|
||||
assert response.headers.get("access-control-allow-origin") == origin
|
||||
|
||||
|
||||
def test_an_unknown_origin_gets_no_header_at_all(client):
|
||||
"""The response still returns 200 with a correct body - which is why this is
|
||||
invisible everywhere except a browser."""
|
||||
response = client.get("/api/health", headers={"Origin": "https://not-listed.example"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "access-control-allow-origin" not in response.headers
|
||||
|
||||
|
||||
def test_a_preflight_from_an_allowed_origin_succeeds(client):
|
||||
response = client.options(
|
||||
"/api/health",
|
||||
headers={"Origin": _allowed_origin(),
|
||||
"Access-Control-Request-Method": "GET",
|
||||
"Access-Control-Request-Headers": "content-type"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers.get("access-control-allow-origin") == _allowed_origin()
|
||||
|
||||
|
||||
def test_a_preflight_from_an_unknown_origin_is_refused(client):
|
||||
"""Starlette answers 400 here. It reads as a malformed request in the log,
|
||||
which is how this was mistaken for a second, unrelated bug."""
|
||||
response = client.options(
|
||||
"/api/health",
|
||||
headers={"Origin": "https://not-listed.example",
|
||||
"Access-Control-Request-Method": "GET"},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
Reference in New Issue
Block a user