"""HTTP surface: contracts, auth, validation and the hardening middleware.""" from __future__ import annotations from collections.abc import AsyncIterator import httpx import pytest from asgi_lifespan import LifespanManager from app.core.middleware import SECURITY_HEADERS from app.core.settings import get_settings from app.main import create_app @pytest.fixture async def client() -> AsyncIterator[httpx.AsyncClient]: app = create_app() async with LifespanManager(app): transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as http: yield http @pytest.fixture def approval_headers() -> dict[str, str]: """A valid bearer header, read from the settings the app checks against. Hardcoding the token made these tests pass locally and fail in CI, where the workflow supplies a different `APPROVAL_TOKEN` — an environment variable beats conftest's `setdefault`, so the app and the test disagreed and every authenticated request came back 401. Deriving it keeps the suite hermetic wherever it runs. """ return {"Authorization": f"Bearer {get_settings().approval_token}"} class TestHealthAndDiscovery: async def test_health_reports_configuration(self, client: httpx.AsyncClient) -> None: response = await client.get("/health") assert response.status_code == 200 body = response.json() assert body["status"] == "ok" assert body["engine"] in ("anthropic", "deterministic") assert body["models"]["supervisor"] assert body["watchlist"] async def test_mcp_tools_are_documented_over_http(self, client: httpx.AsyncClient) -> None: response = await client.get("/v1/mcp/tools") assert response.status_code == 200 tools = response.json() assert {tool["name"] for tool in tools} == { "get_price_history", "get_fundamentals", "compute_metrics", "fetch_rss_news", } async def test_event_kinds_are_published(self, client: httpx.AsyncClient) -> None: response = await client.get("/v1/events/kinds") assert response.status_code == 200 assert "mcp.tool_call" in response.json() class TestSecurityHeaders: async def test_every_response_is_hardened(self, client: httpx.AsyncClient) -> None: response = await client.get("/health") for header, value in SECURITY_HEADERS.items(): assert response.headers.get(header) == value async def test_csp_forbids_script_execution(self, client: httpx.AsyncClient) -> None: response = await client.get("/health") assert "default-src 'none'" in response.headers["Content-Security-Policy"] class TestInputValidation: async def test_unknown_fields_are_rejected(self, client: httpx.AsyncClient) -> None: response = await client.post("/v1/runs", json={"tickers": ["AAPL"], "sneaky": 1}) assert response.status_code == 422 @pytest.mark.parametrize( "tickers", [ [], ["A" * 40], ["AAPL; DROP TABLE runs"], ["../../etc/passwd"], [""], [f"TCK{index}" for index in range(50)], ["AAPL"] * 500, ], ) async def test_hostile_watchlists_are_refused( self, client: httpx.AsyncClient, tickers: list[str] ) -> None: response = await client.post("/v1/runs", json={"tickers": tickers}) assert response.status_code == 422 async def test_duplicate_tickers_collapse_rather_than_fail( self, client: httpx.AsyncClient ) -> None: """A user pasting the same symbol twice is a typo, not an attack.""" from app.api.schemas import CreateRunRequest request = CreateRunRequest(tickers=["AAPL", "aapl", " AAPL "]) assert request.tickers == ["AAPL"] async def test_validation_errors_do_not_echo_the_body(self, client: httpx.AsyncClient) -> None: response = await client.post("/v1/runs", json={"tickers": ["