ComfyUI/tests-unit/server_test/test_api_docs.py

145 lines
5.0 KiB
Python

"""Tests for the OpenAPI spec and API docs routes."""
import os
import pytest
import pytest_asyncio
import yaml
from aiohttp import web
from app.api_docs import SPEC_PATH, SPEC_URL, add_api_docs_routes
pytestmark = pytest.mark.asyncio
def _build_app():
"""Mirror how PromptServer mounts these routes, including the /api prefix.
add_routes() walks the route table and re-registers every RouteDef under an
/api prefix, so both the bare and prefixed forms are served. Reproducing
that here keeps the prefix behaviour covered by tests.
"""
app = web.Application()
routes = web.RouteTableDef()
add_api_docs_routes(routes)
api_routes = web.RouteTableDef()
for route in routes:
if isinstance(route, web.RouteDef):
api_routes.route(route.method, "/api" + route.path)(
route.handler, **route.kwargs
)
app.add_routes(api_routes)
app.add_routes(routes)
return app
@pytest_asyncio.fixture
async def client(aiohttp_client):
return await aiohttp_client(_build_app())
async def test_spec_path_points_at_the_repo_spec():
"""SPEC_PATH must resolve from __file__, not the cwd."""
assert os.path.isfile(SPEC_PATH)
assert os.path.basename(SPEC_PATH) == "openapi.yaml"
async def test_get_spec_returns_yaml(client):
resp = await client.get("/openapi.yaml")
assert resp.status == 200
assert resp.headers["Content-Type"] == "application/yaml"
assert resp.headers["Cache-Control"] == "no-store, must-revalidate"
async def test_spec_body_is_valid_openapi_3(client):
resp = await client.get("/openapi.yaml")
spec = yaml.safe_load(await resp.text())
assert spec["openapi"].startswith("3.")
assert spec["paths"]
async def test_spec_is_also_served_under_the_api_prefix(client):
"""Documents the prefix duplication so a refactor cannot silently break it."""
resp = await client.get("/api/openapi.yaml")
assert resp.status == 200
assert resp.headers["Content-Type"] == "application/yaml"
async def test_missing_spec_returns_404(client, monkeypatch):
monkeypatch.setattr("app.api_docs.SPEC_PATH", "/nonexistent/openapi.yaml")
resp = await client.get("/openapi.yaml")
assert resp.status == 404
async def test_docs_page_returns_html_referencing_the_spec(client):
resp = await client.get("/api-docs")
assert resp.status == 200
assert resp.content_type == "text/html"
body = await resp.text()
assert SPEC_URL in body
async def test_docs_page_spec_url_is_relative(client):
"""A relative URL resolves correctly from both /api-docs and /api/api-docs."""
assert not SPEC_URL.startswith("/")
resp = await client.get("/api/api-docs")
assert resp.status == 200
async def test_docs_page_cannot_execute_requests(client):
"""The local server is unauthenticated, so the docs UI must not fire requests.
Redoc has no request execution at all. Guard against a swap to Swagger UI,
whose "Try it out" would give one-click access to destructive endpoints.
"""
body = (await (await client.get("/api-docs")).text()).lower()
assert "<redoc" in body
assert "swagger" not in body
async def test_docs_page_degrades_when_the_cdn_is_unreachable(client):
"""Offline installs must get the fallback notice, not a blank page.
The viewer bundle is the only part that needs network, so the page carries
an onerror hook that reveals a notice pointing at the locally served spec.
"""
body = await (await client.get("/api-docs")).text()
assert 'onerror=' in body
assert "getElementById('fallback')" in body
assert 'id="fallback"' in body
# The notice has to link the spec, which is served locally.
assert f'<a href="{SPEC_URL}">' in body
async def test_routes_survive_the_static_catch_all(aiohttp_client, tmp_path):
"""web.static('/') is registered last and matches everything.
Registering on PromptServer's route table is what keeps these paths
reachable; this fails if they are ever moved after the catch-all.
"""
(tmp_path / "index.html").write_text("frontend")
(tmp_path / "docs").mkdir()
(tmp_path / "docs" / "node.json").write_text("{}")
app = _build_app()
app.add_routes([web.static("/docs", tmp_path / "docs")])
app.add_routes([web.static("/", tmp_path)])
client = await aiohttp_client(app)
assert (await client.get("/openapi.yaml")).headers["Content-Type"] == (
"application/yaml"
)
assert (await client.get("/api-docs")).content_type == "text/html"
# Embedded node docs and the frontend bundle are untouched.
assert await (await client.get("/docs/node.json")).text() == "{}"
assert await (await client.get("/index.html")).text() == "frontend"
async def test_api_docs_flag_is_off_by_default():
"""Both routes are gated on this flag in PromptServer.add_routes()."""
from comfy.cli_args import parser
assert parser.parse_args([]).enable_api_docs is False
assert parser.parse_args(["--enable-api-docs"]).enable_api_docs is True