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"&#x7c;"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