From 2aaf4de529a8b90731478eb8b0da304a48c8848c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de> Date: Tue, 7 Nov 2023 10:54:43 +0100 Subject: [PATCH] Activate Swagger UI manually #68 --- .gitlab-ci.yml | 1 - README.md | 1 - app/core/config.py | 5 +-- app/main.py | 34 ++++++++++---------- app/tests/api/test_miscellaneous_enpoints.py | 15 ++++++++- 5 files changed, 32 insertions(+), 24 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c9d8e23..12ac690 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,7 +16,6 @@ variables: DB_USER: "random" DB_DATABASE: "random" DB_HOST: "random" - OTLP_GRPC_ENDPOINT: "" cache: paths: diff --git a/README.md b/README.md index cd6f035..977c63b 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,6 @@ user-friendly manner. 👠| Variable | Default | Value | Description | |-----------------------------|----------------------|-----------------------------|----------------------------------------------------------------| | `API_PREFIX` | `/api` | URL path | Prefix before every URL path | -| `BACKEND_CORS_ORIGINS` | `[]` | json formatted list of urls | List of valid CORS origins | | `SQLALCHEMY_VERBOSE_LOGGER` | `false` | `<"true"|"false">` | Enables verbose SQL output.<br>Should be `false` in production | | `OPA_POLICY_PATH` | `/clowm/authz/allow` | URL path | Path to the OPA Policy for Authorization | | `OTLP_GRPC_ENDPOINT` | unset | <hostname / IP> | OTLP compatible endpoint to send traces via gRPC, e.g. Jaeger | diff --git a/app/core/config.py b/app/core/config.py index 28b940e..773fef5 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional from pydantic import AnyHttpUrl, AnyUrl, Field, computed_field from pydantic_settings import BaseSettings, SettingsConfigDict @@ -48,9 +48,6 @@ class Settings(BaseSettings): def PUBLIC_KEY(self) -> str: return _load_public_key(self.public_key_value, self.public_key_file) - # BACKEND_CORS_ORIGINS is a JSON-formatted list of origins - BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = Field([], description="List of all valid CORS origins") - DB_HOST: str = Field(..., description="Host of the database.") DB_USER: str = Field(..., description="Username in the database.") DB_PASSWORD: str = Field(..., description="Password for the database user.") diff --git a/app/main.py b/app/main.py index d2da35c..410e1d8 100644 --- a/app/main.py +++ b/app/main.py @@ -4,7 +4,8 @@ from fastapi import FastAPI, Request, Response, status from fastapi.exception_handlers import http_exception_handler, request_validation_exception_handler from fastapi.exceptions import RequestValidationError, StarletteHTTPException from fastapi.middleware.gzip import GZipMiddleware -from fastapi.responses import JSONResponse +from fastapi.openapi.docs import get_swagger_ui_html +from fastapi.responses import HTMLResponse, JSONResponse from fastapi.routing import APIRoute from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter @@ -69,15 +70,6 @@ FastAPIInstrumentor.instrument_app( app, excluded_urls="health,docs,openapi.json", tracer_provider=trace.get_tracer_provider() ) -# CORS Settings for the API (disabled) -# app.add_middleware( -# CORSMiddleware, -# allow_origins=settings.BACKEND_CORS_ORIGINS, -# allow_credentials=False, -# allow_methods=["*"], -# allow_headers=["*"], -# ) - # Enable gzip compression for large responses app.add_middleware(GZipMiddleware, minimum_size=500) @@ -85,15 +77,23 @@ app.add_middleware(GZipMiddleware, minimum_size=500) app.include_router(api_router) app.include_router(miscellaneous_router) -# Create Custom route for OpenAPI schema -# Hash openapi.json content and save it -m = md5() -m.update(JSONResponse(app.openapi()).body) # ensure the serialization is the same as the one in the route -openapi_hash = m.hexdigest() -del m + +# manually add Swagger UI route +async def swagger_ui_html(req: Request) -> HTMLResponse: + return get_swagger_ui_html( + openapi_url=app.root_path + "/openapi.json", + title=app.title + " - Swagger UI", + swagger_favicon_url="/favicon.ico", + ) -# Route for openapi.json file with ETag header for client cache support +app.add_route("/docs", swagger_ui_html, include_in_schema=False) + +# Hash openapi.json content and ensure the serialization is the same as the one in the route +openapi_hash = md5(JSONResponse(app.openapi()).body).hexdigest() + + +# Create Custom route for OpenAPI schema async def openapi(req: Request) -> Response: # If schema on clients side is still valid, return empty Body with 304 response code (client will use cached body) if req.headers.get("If-None-Match") == openapi_hash: diff --git a/app/tests/api/test_miscellaneous_enpoints.py b/app/tests/api/test_miscellaneous_enpoints.py index a5b8e7a..f86330a 100644 --- a/app/tests/api/test_miscellaneous_enpoints.py +++ b/app/tests/api/test_miscellaneous_enpoints.py @@ -24,7 +24,7 @@ class TestOpenAPIRoute: @pytest.mark.asyncio async def test_openapi_route(self, client: AsyncClient) -> None: """ - Test getting the OpenAPI specification and the caching mechanism. + Test for getting the OpenAPI specification and the caching mechanism. Parameters ---------- @@ -37,3 +37,16 @@ class TestOpenAPIRoute: response2 = await client.get("/openapi.json", headers={"If-None-Match": response1.headers.get("ETag")}) assert response2.status_code == status.HTTP_304_NOT_MODIFIED + + @pytest.mark.asyncio + async def test_swagger_ui_route(self, client: AsyncClient) -> None: + """ + Test for getting the Swagger UI. + + Parameters + ---------- + client : httpx.AsyncClient + HTTP Client to perform the request on. + """ + response1 = await client.get("/docs") + assert response1.status_code == status.HTTP_200_OK -- GitLab