Files
wehub-resource-sync fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:28:29 +08:00

171 lines
6.0 KiB
Python

"""Smoke tests for application/asgi.py.
The goal isn't to re-test Flask or FastMCP internals — it's to catch
regressions in the wiring: mounts resolve, CORS headers emit, lifespan
runs (without it, the /mcp session manager raises "Task group is not
initialized"), routing to ``/`` vs ``/mcp`` doesn't cross paths.
Uses ``starlette.testclient.TestClient`` because it boots the ASGI app
end-to-end and handles the lifespan protocol automatically — ``httpx``
alone does not run lifespan events, which would mask the exact kind of
misconfiguration this test suite exists to catch.
"""
import pytest
@pytest.mark.unit
def test_asgi_app_imports():
from application.asgi import asgi_app
assert asgi_app is not None
@pytest.mark.unit
def test_flask_route_served_through_starlette_mount():
"""GET /api/health should reach the Flask app via a2wsgi and return 200."""
from starlette.testclient import TestClient
from application.asgi import asgi_app
with TestClient(asgi_app) as client:
r = client.get("/api/health")
assert r.status_code == 200
assert r.json() == {"status": "ok"}
@pytest.mark.unit
def test_mcp_endpoint_mounted_and_lifespan_runs():
"""/mcp must be reachable AND the FastMCP session manager must start.
Without ``lifespan=mcp_app.lifespan`` on the outer Starlette app,
every /mcp request raises ``RuntimeError: Task group is not
initialized``. Hitting the endpoint under a real lifespan-aware
client catches that.
"""
from starlette.testclient import TestClient
from application.asgi import asgi_app
with TestClient(asgi_app) as client:
# Minimal MCP initialize request. Doesn't need to succeed — we
# just need a non-404, non-500-with-RuntimeError response to
# confirm the mount + lifespan are both wired.
r = client.post(
"/mcp/",
headers={
"Origin": "http://example.com",
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
},
json={
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {"name": "pytest", "version": "0"},
},
},
)
assert r.status_code != 404, f"/mcp mount unreachable: {r.status_code}"
# A successful initialize returns 200 with a Mcp-Session-Id header.
assert r.status_code == 200
assert "mcp-session-id" in {k.lower() for k in r.headers.keys()}
assert r.headers.get("access-control-expose-headers") == "Mcp-Session-Id"
@pytest.mark.unit
def test_cors_headers_on_flask_route():
"""CORS middleware should emit allow-origin on actual (non-preflight) requests.
``allow_origins=["*"]`` → header value is literal ``*`` (not an echo).
"""
from starlette.testclient import TestClient
from application.asgi import asgi_app
with TestClient(asgi_app) as client:
r = client.get("/api/health", headers={"Origin": "http://example.com"})
assert r.status_code == 200
assert r.headers.get("access-control-allow-origin") == "*"
@pytest.mark.unit
def test_cors_preflight_on_flask_route():
"""OPTIONS preflight on a Flask route should be handled by Starlette CORSMiddleware."""
from starlette.testclient import TestClient
from application.asgi import asgi_app
with TestClient(asgi_app) as client:
r = client.options(
"/api/health",
headers={
"Origin": "http://example.com",
"Access-Control-Request-Method": "GET",
"Access-Control-Request-Headers": "Content-Type",
},
)
assert r.status_code in (200, 204)
assert r.headers.get("access-control-allow-origin") == "*"
assert "GET" in r.headers.get("access-control-allow-methods", "")
@pytest.mark.unit
def test_cors_preflight_allows_patch():
"""PATCH must be in Access-Control-Allow-Methods. The frontend's
apiClient.patch() (used to edit BYOM custom models via PATCH
/api/user/models/<id>) is otherwise blocked at preflight by browsers."""
from starlette.testclient import TestClient
from application.asgi import asgi_app
with TestClient(asgi_app) as client:
r = client.options(
"/api/health",
headers={
"Origin": "http://example.com",
"Access-Control-Request-Method": "PATCH",
"Access-Control-Request-Headers": "Content-Type, Authorization",
},
)
assert r.status_code in (200, 204)
assert "PATCH" in r.headers.get("access-control-allow-methods", "")
@pytest.mark.unit
def test_cors_preflight_on_mcp_route():
"""Browser clients hitting /mcp should be allowed to send session headers."""
from starlette.testclient import TestClient
from application.asgi import asgi_app
with TestClient(asgi_app) as client:
r = client.options(
"/mcp/",
headers={
"Origin": "http://example.com",
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": (
"Authorization, Content-Type, Mcp-Session-Id"
),
},
)
assert r.status_code in (200, 204)
assert r.headers.get("access-control-allow-origin") == "*"
assert "Mcp-Session-Id" in r.headers.get("access-control-allow-headers", "")
@pytest.mark.unit
def test_wsgi_threadpool_sized_from_settings():
"""The Flask thread pool is the app's request-capacity ceiling —
it must be operator-tunable (prod incident 2026-07-05→07: 32 slots
exhausted by SSE holders starved all other requests)."""
from application import asgi
from application.core.settings import settings
assert asgi._WSGI_THREADPOOL == int(settings.WSGI_THREADPOOL_WORKERS)
assert asgi._WSGI_THREADPOOL >= 64