205 lines
7.0 KiB
Python
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"
|