diff --git a/tests/test_openapi_websocket_exclusion.py b/tests/test_openapi_websocket_exclusion.py new file mode 100644 index 0000000..08d2d1a --- /dev/null +++ b/tests/test_openapi_websocket_exclusion.py @@ -0,0 +1,38 @@ +"""Regression test: WebSocket routes used to show up in /openapi.json as an +empty, spec-invalid Path Item Object ({}) -- PathItem has no "websocket" +field, so setattr(path_item, "websocket", op) silently created a dangling +attribute to_dict() never serialized, and the path stayed in the schema +with no operations at all. +""" + +import asyncio + +from velocix import TestClient, Velocix +from velocix.websocket.connection import WebSocket + + +def _run(coro): + return asyncio.run(coro) + + +def test_websocket_route_excluded_from_openapi_schema(): + app = Velocix() + + @app.get("/posts") + async def list_posts(): + return {"posts": []} + + @app.websocket("/ws/posts/{post_id}") + async def watch_post(websocket: WebSocket): + await websocket.accept() + await websocket.close() + + async def scenario(): + async with TestClient(app) as client: + schema = (await client.get("/openapi.json")).json() + paths = schema.get("paths", {}) + assert "/posts" in paths + assert "get" in paths["/posts"] + assert "/ws/posts/{post_id}" not in paths + + _run(scenario()) diff --git a/velocix/core/app.py b/velocix/core/app.py index e26baae..0c597c4 100644 --- a/velocix/core/app.py +++ b/velocix/core/app.py @@ -937,6 +937,12 @@ async def openapi_handler(request: Request) -> Response: # missing from the docs entirely or, once hit, showed up # under the literal ID that happened to hit them first. for method, path, handler, _name in self.router._registered: + if method == "WEBSOCKET": + # OpenAPI 3.x has no operation concept for + # WebSocket routes; PathItem has no matching + # field, so this always produced an empty (and + # spec-invalid) {} entry. + continue if getattr(handler, "__route_include_in_schema__", True) is False: continue if path in (self.openapi_url, self.docs_url, self.redoc_url):