Files

205 lines
7.0 KiB
Python

"""Regression coverage for the unauthenticated Gateway health probes."""
from __future__ import annotations
from collections import defaultdict
from collections.abc import Iterator
from types import SimpleNamespace
from typing import Any
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.gateway import auth_middleware
from app.gateway.app import app as gateway_app
from app.gateway.auth.token_source import get_explicit_or_session_token
from app.gateway.auth_middleware import PUBLIC_HEALTH_PATHS, AuthMiddleware
def _effective_gateway_routes() -> Iterator[Any]:
"""Expand FastAPI's lazy included routers into their effective routes."""
for route in gateway_app.router.routes:
route_contexts = getattr(route, "effective_route_contexts", None)
if callable(route_contexts):
yield from route_contexts()
else:
yield route
def _endpoint_name(route: Any) -> str:
endpoint = getattr(route, "endpoint", None)
module = getattr(endpoint, "__module__", "<unknown>")
name = getattr(endpoint, "__name__", repr(endpoint))
return f"{module}.{name}"
def _exact_routes(method: str, path: str) -> list[Any]:
return [
route
for route in _effective_gateway_routes()
if route.path == path and method in (getattr(route, "methods", None) or set())
]
def _first_matching_route(method: str, path: str) -> Any:
for route in _effective_gateway_routes():
methods = getattr(route, "methods", None) or set()
path_regex = getattr(route, "path_regex", None)
if method in methods and path_regex is not None and path_regex.fullmatch(path):
return route
raise AssertionError(f"No route matched {method} {path}")
def _health_only_app() -> FastAPI:
app = FastAPI()
async def health_check() -> dict[str, str]:
return {"status": "healthy", "service": "deer-flow-gateway"}
for path in PUBLIC_HEALTH_PATHS:
app.add_api_route(path, health_check, methods=["GET", "HEAD"])
app.add_middleware(AuthMiddleware)
return app
@pytest.mark.parametrize("path", sorted(PUBLIC_HEALTH_PATHS))
@pytest.mark.parametrize("method", ["GET", "HEAD"])
def test_health_probe_is_public_when_auth_is_enabled(
monkeypatch: pytest.MonkeyPatch,
path: str,
method: str,
) -> None:
monkeypatch.setattr(auth_middleware, "is_auth_disabled", lambda: False)
with TestClient(_health_only_app()) as client:
client.cookies.set("access_token", "invalid-token")
response = client.request(
method,
path,
headers={"Authorization": "Bearer invalid-token"},
)
assert response.status_code == 200
if method == "GET":
assert response.json() == {"status": "healthy", "service": "deer-flow-gateway"}
else:
assert response.content == b""
@pytest.mark.parametrize("path", sorted(PUBLIC_HEALTH_PATHS))
def test_health_probe_allows_a_trailing_slash(
monkeypatch: pytest.MonkeyPatch,
path: str,
) -> None:
monkeypatch.setattr(auth_middleware, "is_auth_disabled", lambda: False)
with TestClient(_health_only_app(), follow_redirects=False) as client:
response = client.get(f"{path}/")
# FastAPI redirects to the canonical route; importantly, auth must not turn
# the request into a 401 before routing can perform that redirect.
assert response.status_code == 307
@pytest.mark.parametrize("path", sorted(PUBLIC_HEALTH_PATHS))
def test_health_probe_is_public_behind_a_reverse_proxy_prefix(
monkeypatch: pytest.MonkeyPatch,
path: str,
) -> None:
monkeypatch.setattr(auth_middleware, "is_auth_disabled", lambda: False)
with TestClient(_health_only_app(), root_path="/deerflow") as client:
response = client.get(f"/deerflow{path}")
assert response.status_code == 200
def test_gateway_registers_every_public_health_path_for_get_and_head() -> None:
methods_by_path = {
route.path: route.methods
for route in gateway_app.routes
if hasattr(route, "methods")
}
for path in PUBLIC_HEALTH_PATHS:
assert {"GET", "HEAD"} <= methods_by_path[path]
def test_gateway_method_path_pairs_are_unique() -> None:
endpoints_by_method_path: dict[tuple[str, str], list[str]] = defaultdict(list)
for route in _effective_gateway_routes():
path = getattr(route, "path", None)
if path is None:
continue
for method in getattr(route, "methods", None) or set():
endpoints_by_method_path[(method, path)].append(_endpoint_name(route))
duplicates = {
method_path: endpoints
for method_path, endpoints in endpoints_by_method_path.items()
if len(endpoints) > 1
}
assert duplicates == {}
def test_checkpoint_and_event_store_message_contracts_are_distinct() -> None:
checkpoint_path = "/api/threads/{thread_id}/messages"
event_store_path = "/api/threads/{thread_id}/run-messages"
checkpoint_routes = _exact_routes("GET", checkpoint_path)
event_store_routes = _exact_routes("GET", event_store_path)
assert [_endpoint_name(route) for route in checkpoint_routes] == [
"app.gateway.routers.threads.get_thread_messages"
]
assert [_endpoint_name(route) for route in event_store_routes] == [
"app.gateway.routers.thread_runs.list_thread_run_messages"
]
assert _endpoint_name(
_first_matching_route("GET", "/api/threads/thread-one/messages")
) == "app.gateway.routers.threads.get_thread_messages"
assert _endpoint_name(
_first_matching_route("GET", "/api/threads/thread-one/run-messages")
) == "app.gateway.routers.thread_runs.list_thread_run_messages"
def test_non_health_route_still_requires_auth(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(auth_middleware, "is_auth_disabled", lambda: False)
app = _health_only_app()
@app.get("/private")
async def private() -> dict[str, bool]:
return {"ok": True}
with TestClient(app) as client:
response = client.get("/private")
assert response.status_code == 401
def _auth_request(*, authorization: str = "", cookie: str = "") -> SimpleNamespace:
headers = {"authorization": authorization} if authorization else {}
cookies = {"access_token": cookie} if cookie else {}
return SimpleNamespace(headers=headers, cookies=cookies)
def test_explicit_bearer_token_wins_over_shared_cookie() -> None:
"""Another tab's cookie must not override this tab's explicit token."""
request = _auth_request(authorization="Bearer current-tab", cookie="other-tab")
assert get_explicit_or_session_token(request) == "current-tab"
def test_cookie_remains_the_fallback_for_cookie_only_clients() -> None:
request = _auth_request(cookie="browser-session")
assert get_explicit_or_session_token(request) == "browser-session"
def test_malformed_authorization_header_does_not_hide_valid_cookie() -> None:
request = _auth_request(authorization="Basic abc", cookie="browser-session")
assert get_explicit_or_session_token(request) == "browser-session"