fix: wildcard CORS for public banner API endpoints
Some checks failed
CI / Detect changes (push) Has been cancelled
CI / API Lint (push) Has been cancelled
CI / API Tests (push) Has been cancelled
CI / Scanner Lint (push) Has been cancelled
CI / Scanner Tests (push) Has been cancelled
CI / Banner Lint & Typecheck (push) Has been cancelled
CI / Banner Tests (push) Has been cancelled
CI / Banner Build (push) Has been cancelled
CI / Admin UI Typecheck (push) Has been cancelled
CI / Admin UI Tests (push) Has been cancelled
CI / Admin UI Build (push) Has been cancelled
Some checks failed
CI / Detect changes (push) Has been cancelled
CI / API Lint (push) Has been cancelled
CI / API Tests (push) Has been cancelled
CI / Scanner Lint (push) Has been cancelled
CI / Scanner Tests (push) Has been cancelled
CI / Banner Lint & Typecheck (push) Has been cancelled
CI / Banner Tests (push) Has been cancelled
CI / Banner Build (push) Has been cancelled
CI / Admin UI Typecheck (push) Has been cancelled
CI / Admin UI Tests (push) Has been cancelled
CI / Admin UI Build (push) Has been cancelled
Replace the fragile per-site dynamic CORS middleware with a public banner CORS middleware that allows non-credentialed wildcard CORS only for banner endpoints: - /api/v1/config/sites/* - /api/v1/translations/* - /api/v1/consent/ Admin/auth endpoints remain governed by the normal ALLOWED_ORIGINS based CORSMiddleware. Add regression tests for public GET/preflight behavior and for avoiding wildcard CORS on non-public endpoints.
This commit is contained in:
84
apps/api/tests/test_public_banner_cors.py
Normal file
84
apps/api/tests/test_public_banner_cors.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""CORS behavior for public banner endpoints.
|
||||
|
||||
The banner API is embedded on merchant websites, so these public endpoints
|
||||
must be readable cross-origin without relying on the admin ALLOWED_ORIGINS
|
||||
setting.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from src.db import get_db
|
||||
from src.main import create_app
|
||||
|
||||
|
||||
def _db_returning_not_found() -> AsyncMock:
|
||||
session = AsyncMock()
|
||||
result = MagicMock()
|
||||
result.scalar_one_or_none.return_value = None
|
||||
session.execute = AsyncMock(return_value=result)
|
||||
return session
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_public_config_get_allows_any_merchant_origin():
|
||||
app = create_app()
|
||||
db = _db_returning_not_found()
|
||||
|
||||
async def _override_get_db():
|
||||
yield db
|
||||
|
||||
app.dependency_overrides[get_db] = _override_get_db
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
resp = await client.get(
|
||||
f"/api/v1/config/sites/{uuid.uuid4()}",
|
||||
headers={"Origin": "https://www.dealplustech.co.th"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 404
|
||||
assert resp.headers["access-control-allow-origin"] == "*"
|
||||
assert "access-control-allow-credentials" not in resp.headers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_public_consent_preflight_allows_json_post_from_any_merchant_origin():
|
||||
app = create_app()
|
||||
transport = ASGITransport(app=app)
|
||||
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
resp = await client.options(
|
||||
"/api/v1/consent/",
|
||||
headers={
|
||||
"Origin": "https://www.dealplustech.co.th",
|
||||
"Access-Control-Request-Method": "POST",
|
||||
"Access-Control-Request-Headers": "content-type",
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 204
|
||||
assert resp.headers["access-control-allow-origin"] == "*"
|
||||
assert "POST" in resp.headers["access-control-allow-methods"]
|
||||
assert "content-type" in resp.headers["access-control-allow-headers"].lower()
|
||||
assert "access-control-allow-credentials" not in resp.headers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_public_endpoint_does_not_get_public_wildcard_cors():
|
||||
app = create_app()
|
||||
transport = ASGITransport(app=app)
|
||||
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
resp = await client.get(
|
||||
"/health",
|
||||
headers={"Origin": "https://www.dealplustech.co.th"},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.headers.get("access-control-allow-origin") != "*"
|
||||
Reference in New Issue
Block a user