Ver Fonte

Tests: no duplicate OpenAPI operation ids

- manifest.json, sw.js, sw-register.js and camera stop used
  api_route(methods=[...]), which gives both methods the same OpenAPI
  operation id; FastAPI warned about it whenever /openapi.json was
  built. One decorator per method instead; GET, HEAD and POST answer
  as before.
- The route auth backstop now lists the HEAD routes of the three PWA
  files explicitly; before, a combined route was only checked as GET.
- New test: the OpenAPI schema builds without warnings and every
  operation id is unique.
maziggy há 1 dia atrás
pai
commit
182d6e6337

+ 2 - 1
backend/app/api/routes/camera.py

@@ -1453,7 +1453,8 @@ async def camera_stream(
     )
 
 
-@router.api_route("/{printer_id}/camera/stop", methods=["GET", "POST"])
+@router.get("/{printer_id}/camera/stop")
+@router.post("/{printer_id}/camera/stop")
 async def stop_camera_stream(
     printer_id: int,
     _: User | None = RequirePrinterPermissionIfAuthEnabled(Permission.CAMERA_VIEW),

+ 8 - 4
backend/app/main.py

@@ -13010,8 +13010,10 @@ async def health_check():
 # GET + HEAD on the three PWA bootstrap routes (#1460). Scanners and a plain
 # `curl -I` use HEAD; FastAPI's @app.get only registers GET, so HEAD answers
 # with 405 Method Not Allowed and shows up as a "broken manifest" red herring
-# in deployment debugging.
-@app.api_route("/manifest.json", methods=["GET", "HEAD"])
+# in deployment debugging. One decorator per method, not api_route(methods=
+# [...]): api_route gives both methods the same OpenAPI operation id.
+@app.get("/manifest.json")
+@app.head("/manifest.json")
 async def serve_manifest():
     """Serve PWA manifest."""
     manifest_file = app_settings.static_dir / "manifest.json"
@@ -13020,7 +13022,8 @@ async def serve_manifest():
     return {"error": "Manifest not found"}
 
 
-@app.api_route("/sw.js", methods=["GET", "HEAD"])
+@app.get("/sw.js")
+@app.head("/sw.js")
 async def serve_service_worker():
     """Serve service worker."""
     sw_file = app_settings.static_dir / "sw.js"
@@ -13033,7 +13036,8 @@ async def serve_service_worker():
     return {"error": "Service worker not found"}
 
 
-@app.api_route("/sw-register.js", methods=["GET", "HEAD"])
+@app.get("/sw-register.js")
+@app.head("/sw-register.js")
 async def serve_sw_register():
     """Serve the service-worker registration bootstrap script.
 

+ 21 - 0
backend/tests/unit/test_openapi_operation_ids.py

@@ -0,0 +1,21 @@
+"""Every operation in the OpenAPI schema has its own operation id.
+
+A route registered with api_route(methods=[A, B]) gives both methods the
+same id, and FastAPI only warns about it while it builds /openapi.json.
+Register one decorator per method instead.
+"""
+
+import warnings
+
+from fastapi.openapi.utils import get_openapi
+
+from backend.app.main import app
+
+
+def test_operation_ids_are_unique():
+    with warnings.catch_warnings():
+        warnings.simplefilter("error")
+        schema = get_openapi(title=app.title, version=app.version, routes=app.routes)
+
+    ids = [op["operationId"] for path in schema["paths"].values() for op in path.values()]
+    assert len(ids) == len(set(ids)), sorted({i for i in ids if ids.count(i) > 1})

+ 4 - 0
backend/tests/unit/test_route_auth_coverage.py

@@ -141,6 +141,10 @@ _PUBLIC_ROUTES: frozenset[tuple[str, str]] = frozenset(
         ("GET", "/manifest.json"),
         ("GET", "/sw-register.js"),
         ("GET", "/sw.js"),
+        # HEAD twins of the three PWA files (#1460), one route per method
+        ("HEAD", "/manifest.json"),
+        ("HEAD", "/sw-register.js"),
+        ("HEAD", "/sw.js"),
         # SPA catch-all — serves index.html for client-side routing. No backend data path.
         ("GET", "/{full_path:path}"),
         # ---- WebSocket routes ----