Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies = [
"loguru>=0.7.3",
"pydantic>=2.11.7",
"pydantic-settings>=2.10.1",
"pyjwt>=2.10.1",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

PyJWT latest version PyPI 2.10.1

💡 Result:

The latest PyJWT release on PyPI is 2.10.1 (uploaded Nov 28, 2024). [1][2]

Sources:
[1] PyPI project page for PyJWT.
[2] PyJWT changelog.


🌐 Web query:

PyJWT security vulnerabilities CVE 2024 2025

💡 Result:

Summary — known CVEs for PyJWT (2024–2025)

  • CVE-2024-53861 — Issuer (iss) partial-comparison bug introduced in PyJWT 2.10.0 that allowed partial matches (e.g., "abc" matched "abc"). Patched in 2.10.1; upgrade recommended. [1][2]

  • CVE-2025-45768 — Reported July 31, 2025: weak/insufficient key length handling in JWS (CWE-311) affecting pyjwt 2.10.1 (disputed by the supplier). Scoring classified as High (CVSS ~7.0); vendors/trackers advise remediation or upgrading per vendor guidance. [3][4]

Recommended actions

  • Upgrade to the vendor-fixed release(s) where available (CVE-2024-53861 → 2.10.1). For CVE-2025-45768 follow your vendor/security bulletin for the fixed version or mitigation guidance (enforce minimum key lengths, validate JWS algorithm/key choices). [1][3]

Sources

  1. NVD entry for CVE-2024-53861 (pyjwt issuer partial-comparison; fixed in 2.10.1). [NVD]. [1]
  2. Debian / security trackers summarizing CVE-2024-53861 and fix. [2]
  3. CVE-2025-45768 advisory / vulnerability entries (INCIBE / Wiz summary of weak key-length issue; published July 31, 2025). [3][4]

If you want, I can: (a) fetch the PyJWT project advisory/GitHub commits for each CVE, (b) check whether a fixed PyPI release for CVE-2025-45768 exists, or (c) produce a remediation checklist for your codebase.


Address CVE-2025-45768 security vulnerability in PyJWT 2.10.1 before merging.

PyJWT 2.10.1 is the latest stable release, but it contains an active HIGH-severity vulnerability (CVE-2025-45768) related to weak/insufficient key length handling in JWS (CVSS ~7.0). Until a patched version is available, enforce strict minimum key length requirements and validate all JWS algorithm/key choices in the authentication implementation.

🤖 Prompt for AI Agents
In pyproject.toml around line 16, the PyJWT dependency ("pyjwt>=2.10.1") is
flagged as vulnerable (CVE-2025-45768); instead of leaving the vulnerable spec,
update the project to either pin to a patched release when available (e.g.,
change the constraint to the patched minimum version once released) and/or
temporarily tighten requirements by replacing the loose spec with a constrained
range that excludes the vulnerable release, and implement runtime safeguards in
the auth code that validate JWS algorithm and key choices: explicitly reject
"none" alg, require HS keys to meet a minimum byte length for HS256/384/512,
require RSA keys >= 2048 bits and EC keys using secure curves only, add unit
tests exercising algorithm/key validation and update CI dependency scans to fail
the build if a vulnerable PyJWT is installed until a patched version is adopted.

"pytz>=2025.2",
"ray[serve]==2.53.0",
"sentry-sdk[fastapi]>=2.33.0",
Expand Down
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ env =
; plugins__proxy__force_stream_apis=[]
sentry__enable=false
test__silent=true
auth__rules={{"/api/v1/echo":["i_am_general_auth_keys"],"/api/v1/proxy/remote":["i_am_local_proxy_auth_keys"]}}
auth__rules={{"/api/v1/openapi.json":["i_am_docs_key"],"/api/v1/echo":["i_am_general_auth_keys"],"/api/v1/proxy/remote":["i_am_local_proxy_auth_keys"]}}

asyncio_mode = auto
2 changes: 1 addition & 1 deletion ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ ignore = [
]

[lint.per-file-ignores]
"tests/**/*.py" = ["S101", "ANN201", "ANN001", "ANN002"]
"tests/**/*.py" = ["S101", "ANN201", "ANN001", "ANN002", "ANN003"]

[lint.flake8-builtins]
builtins-ignorelist = ["input", "id", "bytes", "type"]
Expand Down
43 changes: 32 additions & 11 deletions src/framex/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import secrets
from typing import Any, Literal

from pydantic import BaseModel, Field
Expand Down Expand Up @@ -45,24 +46,44 @@ class ServerConfig(BaseModel):
excluded_log_paths: list[str] = Field(default_factory=list)
ingress_config: dict[str, Any] = Field(default_factory=lambda: {"max_ongoing_requests": 60})

# docs config
docs_user: str = "admin"
docs_password: str = ""

def model_post_init(self, __context: Any) -> None: # pragma: no cover
if self.docs_password == "":
self.docs_password = "admin" # noqa: S105
from framex.log import logger

logger.warning("No docs_password set, fallback to default password: admin")


class TestConfig(BaseModel):
disable_record_request: bool = False
silent: bool = False


class OauthConfig(BaseModel):
client_id: str = ""
client_secret: str = ""
authorization_url: str = ""
redirect_uri: str = ""
base_url: str = ""

token_url: str = ""
user_info_url: str = ""
app_url: str = ""

jwt_secret: str = ""
jwt_algorithm: str = "HS256"

@property
def call_back_url(self) -> str:
return f"{self.app_url}{self.redirect_uri}"

def model_post_init(self, context: Any) -> None:
super().model_post_init(context)
if not self.authorization_url:
self.authorization_url = f"{self.base_url}/oauth/authorize"
if not self.token_url:
self.token_url = f"{self.base_url}/oauth/token"
if not self.user_info_url:
self.user_info_url = f"{self.base_url}/api/v4/user"
if not self.jwt_secret:
self.jwt_secret = secrets.token_urlsafe(32)


class AuthConfig(BaseModel):
oauth: OauthConfig | None = Field(default=None)
rules: dict[str, list[str]] = Field(default_factory=dict)

def _is_url_protected(self, url: str) -> bool:
Expand Down
25 changes: 7 additions & 18 deletions src/framex/driver/application.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Module containing FastAPI instance related functions and classes."""

import json
import secrets
from collections.abc import Callable
from contextlib import asynccontextmanager
from datetime import UTC, datetime
Expand All @@ -13,7 +12,6 @@
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
from fastapi.openapi.utils import get_openapi
from fastapi.responses import HTMLResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from starlette import status
from starlette.concurrency import iterate_in_threadpool
from starlette.exceptions import HTTPException
Expand All @@ -24,6 +22,7 @@

from framex.config import settings
from framex.consts import API_STR, DOCS_URL, OPENAPI_URL, PROJECT_NAME, REDOC_URL, VERSION
from framex.driver.auth import authenticate, oauth_callback
from framex.utils import format_uptime

FRAME_START_TIME = datetime.now(tz=UTC)
Expand Down Expand Up @@ -95,22 +94,12 @@ async def _on_start(deployment: DeploymentHandle) -> None:
redirect_slashes=False,
)

security = HTTPBasic(realm="Swagger Docs")

def authenticate(credentials: HTTPBasicCredentials = Depends(security)) -> str:
correct_username = secrets.compare_digest(credentials.username, settings.server.docs_user)
correct_password = secrets.compare_digest(credentials.password, settings.server.docs_password)

if not (correct_username and correct_password):
from framex.log import logger

logger.warning(f"Failed authentication attempt for user: {credentials.username}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect user or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
if settings.auth.oauth:
application.add_api_route(
settings.auth.oauth.redirect_uri,
oauth_callback,
methods=["GET"],
)

@application.get(DOCS_URL, include_in_schema=False)
async def get_documentation(_: Annotated[str, Depends(authenticate)]) -> HTMLResponse:
Expand Down
128 changes: 128 additions & 0 deletions src/framex/driver/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from datetime import UTC, datetime, timedelta

import httpx
import jwt
from fastapi import Depends, HTTPException, Response, status
from fastapi.responses import RedirectResponse
from fastapi.security import APIKeyHeader
from starlette.requests import Request

from framex.config import settings
from framex.consts import DOCS_URL

api_key_header = APIKeyHeader(name="Authorization", auto_error=False)


def create_jwt(payload: dict) -> str:
if not settings.auth.oauth:
raise RuntimeError("OAuth not configured")

now_utc = datetime.now(UTC)

payload.update(
{
"iat": int(now_utc.timestamp()),
"exp": int((now_utc + timedelta(hours=24)).timestamp()),
}
)
return jwt.encode(payload, settings.auth.oauth.jwt_secret, algorithm=settings.auth.oauth.jwt_algorithm)


def auth_jwt(request: Request) -> bool:
if not settings.auth.oauth:
return False

token = request.cookies.get("token")
if not token:
return False

try:
jwt.decode(
token,
settings.auth.oauth.jwt_secret,
algorithms=[settings.auth.oauth.jwt_algorithm],
)
return True
except (jwt.InvalidTokenError, jwt.ExpiredSignatureError):
return False


def authenticate(request: Request, api_key: str | None = Depends(api_key_header)) -> None:
if settings.auth.oauth:
if token := request.cookies.get("token"):
try:
jwt.decode(
token,
settings.auth.oauth.jwt_secret,
algorithms=[settings.auth.oauth.jwt_algorithm],
)
return

except Exception as e:
from framex.log import logger

logger.warning(f"JWT decode failed: {e}")
Comment on lines +61 to +64
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace broad exception catch with specific JWT exceptions.

Catching Exception on line 61 masks potential bugs and security issues. Specifically catch JWT-related exceptions instead.

🔎 Proposed fix
-            except Exception as e:
+            except (jwt.InvalidTokenError, jwt.ExpiredSignatureError) as e:
                 from framex.log import logger
 
                 logger.warning(f"JWT decode failed: {e}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except Exception as e:
from framex.log import logger
logger.warning(f"JWT decode failed: {e}")
except (jwt.InvalidTokenError, jwt.ExpiredSignatureError) as e:
from framex.log import logger
logger.warning(f"JWT decode failed: {e}")
🧰 Tools
🪛 Ruff (0.14.10)

61-61: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In src/framex/driver/auth.py around lines 61 to 64, the code currently catches a
broad Exception when JWT decoding fails; replace this with specific PyJWT
exceptions (for example jwt.ExpiredSignatureError, jwt.DecodeError and/or
jwt.InvalidTokenError) by importing them from the jwt library and handling them
explicitly (log the appropriate warning for token expiration or invalid/failed
decode), and allow any other unexpected exceptions to propagate so they aren't
silently swallowed.


if api_key and api_key in (settings.auth.get_auth_keys(request.url.path) or []):
return

raise HTTPException(
status_code=status.HTTP_302_FOUND,
headers={
"Location": (
f"{settings.auth.oauth.authorization_url}"
f"?client_id={settings.auth.oauth.client_id}"
"&response_type=code"
f"&redirect_uri={settings.auth.oauth.call_back_url}"
"&scope=read_user"
)
},
)
Comment on lines +50 to +80
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for settings.auth.oauth configuration and usage patterns
rg -n "settings\.auth\.oauth" --type py -A 3 -B 3

Repository: touale/FrameX-kit

Length of output: 9430


🏁 Script executed:

#!/bin/bash
# Search for API key authentication and get_auth_keys usage
rg -n "get_auth_keys" --type py -A 3 -B 3

Repository: touale/FrameX-kit

Length of output: 4485


🏁 Script executed:

#!/bin/bash
# Look for the settings definition and OAuth configuration
fd -e py | xargs grep -l "class.*Settings\|auth.*oauth" | head -20

Repository: touale/FrameX-kit

Length of output: 166


🏁 Script executed:

#!/bin/bash
# Search for authenticate function calls and other auth functions
rg -n "def authenticate" --type py -A 10

Repository: touale/FrameX-kit

Length of output: 782


🏁 Script executed:

#!/bin/bash
# Check the full authenticate function and surrounding context
sed -n '50,82p' src/framex/driver/auth.py

Repository: touale/FrameX-kit

Length of output: 1181


🏁 Script executed:

#!/bin/bash
# Look for how api_key_header dependency works
rg -n "api_key_header" --type py -B 5 -A 5

Repository: touale/FrameX-kit

Length of output: 3206


🏁 Script executed:

#!/bin/bash
# Search for any tests or logic handling authenticate when oauth is None
rg -n "def test.*authenticate\|authenticate.*oauth.*None\|settings\.auth\s*=" --type py -A 3 -B 1

Repository: touale/FrameX-kit

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Look at how authenticate is used as a dependency in the application
rg -n "Depends(authenticate)" --type py -B 3 -A 3

Repository: touale/FrameX-kit

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check the settings.auth configuration to understand defaults
cat src/framex/config.py | head -150

Repository: touale/FrameX-kit

Length of output: 4600


🏁 Script executed:

#!/bin/bash
# Get the full settings.auth configuration definition
rg -n "class AuthConfig\|oauth\s*:" src/framex/config.py -A 20 | head -80

Repository: touale/FrameX-kit

Length of output: 43


API key validation is completely skipped when OAuth is not configured.

The authenticate() function gates all authentication logic—including API key validation—behind if settings.auth.oauth:. Since OAuth is optional by design (oauth: OauthConfig | None), disabling it breaks API-key-only authentication entirely. The API key parameter is extracted but never validated in this scenario. The codebase demonstrates that API key validation should work independently of OAuth (see ingress.py line 117). Either move API key validation outside the OAuth block or add fallback handling for when OAuth is disabled.

🧰 Tools
🪛 Ruff (0.14.10)

59-59: Consider moving this statement to an else block

(TRY300)


61-61: Do not catch blind exception: Exception

(BLE001)



async def oauth_callback(code: str) -> Response:
if not settings.auth.oauth: # pragma: no cover
raise RuntimeError("OAuth not configured")

async with httpx.AsyncClient() as client:
resp = await client.post(
settings.auth.oauth.token_url,
data={
"client_id": settings.auth.oauth.client_id,
"client_secret": settings.auth.oauth.client_secret,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": settings.auth.oauth.call_back_url,
},
)
Comment on lines +87 to +97
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add timeout to token exchange POST request.

The POST request to the token URL (line 88) does not have a timeout, while the subsequent GET request to user_info_url does (line 107). Without a timeout, the request can hang indefinitely, degrading reliability and potentially causing resource exhaustion.

🔎 Proposed fix
         resp = await client.post(
             settings.auth.oauth.token_url,
             data={
                 "client_id": settings.auth.oauth.client_id,
                 "client_secret": settings.auth.oauth.client_secret,
                 "grant_type": "authorization_code",
                 "code": code,
                 "redirect_uri": settings.auth.oauth.call_back_url,
             },
+            timeout=10,
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async with httpx.AsyncClient() as client:
resp = await client.post(
settings.auth.oauth.token_url,
data={
"client_id": settings.auth.oauth.client_id,
"client_secret": settings.auth.oauth.client_secret,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": settings.auth.oauth.call_back_url,
},
)
async with httpx.AsyncClient() as client:
resp = await client.post(
settings.auth.oauth.token_url,
data={
"client_id": settings.auth.oauth.client_id,
"client_secret": settings.auth.oauth.client_secret,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": settings.auth.oauth.call_back_url,
},
timeout=10,
)
🤖 Prompt for AI Agents
In src/framex/driver/auth.py around lines 87 to 97, the token exchange POST
request is missing a timeout which can cause the call to hang; update the
client.post invocation to include an explicit timeout (match the same timeout
used for the subsequent user_info GET request) by passing the timeout argument
to client.post so the request fails fast and prevents resource exhaustion.

resp.raise_for_status()
token_resp = resp.json()

if not (auth_token := token_resp.get("access_token")): # pragma: no cover
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="GitLab token exchange failed")

resp = await client.get(
settings.auth.oauth.user_info_url,
headers={"Authorization": f"Bearer {auth_token}"},
timeout=10,
)
resp.raise_for_status()
user_resp = resp.json()

if not (username := user_resp.get("username")): # pragma: no cover
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Failed to get GitLab user")

user_info = {
"message": f"Welcome {username}",
"username": username,
"email": user_resp.get("email"),
}

res = RedirectResponse(url=DOCS_URL, status_code=status.HTTP_302_FOUND)
res.set_cookie(
"token",
create_jwt(user_info),
httponly=True,
samesite="lax",
)
Comment on lines +122 to +127
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add secure=True flag to JWT cookie for production.

The JWT cookie is set without the secure flag, which means it can be transmitted over unencrypted HTTP connections. This exposes the authentication token to potential interception.

🔎 Proposed fix
     res.set_cookie(
         "token",
         create_jwt(user_info),
         httponly=True,
         samesite="lax",
+        secure=True,
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
res.set_cookie(
"token",
create_jwt(user_info),
httponly=True,
samesite="lax",
)
res.set_cookie(
"token",
create_jwt(user_info),
httponly=True,
samesite="lax",
secure=True,
)
🤖 Prompt for AI Agents
In src/framex/driver/auth.py around lines 122 to 127, the JWT cookie is being
set without the secure flag; update the res.set_cookie call to include
secure=True when running in production (e.g., check an environment/config flag
like settings.DEBUG or os.getenv("ENV") != "development", or detect request
scheme == "https") so the cookie is only sent over HTTPS; keep httponly and
samesite as-is and ensure the conditional sets secure=False only for local/dev
to allow testing.

return res
9 changes: 4 additions & 5 deletions src/framex/driver/ingress.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,23 @@
from enum import Enum
from typing import Any

from fastapi import Depends, HTTPException, Response, status
from fastapi import Depends, HTTPException, Request, Response, status
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.routing import APIRoute
from fastapi.security import APIKeyHeader
from pydantic import create_model
from ray.serve.handle import DeploymentHandle
from starlette.routing import Route

from framex.adapter import get_adapter
from framex.consts import BACKEND_NAME
from framex.driver.application import create_fastapi_application
from framex.driver.auth import api_key_header, auth_jwt
from framex.driver.decorator import api_ingress
from framex.log import setup_logger
from framex.plugin.model import ApiType, PluginApi
from framex.utils import escape_tag, safe_error_message

app = create_fastapi_application()
api_key_header = APIKeyHeader(name="Authorization", auto_error=True)


@app.get("/health")
Expand Down Expand Up @@ -114,8 +113,8 @@ async def route_handler(response: Response, model: Model = Depends()) -> Any: #
if auth_keys is not None:
logger.debug(f"API({path}) with tags {tags} requires auth.")

def _verify_api_key(api_key: str = Depends(api_key_header)) -> None:
if api_key not in auth_keys:
def _verify_api_key(request: Request, api_key: str | None = Depends(api_key_header)) -> None:
if (api_key is None or api_key not in auth_keys) and (not auth_jwt(request)):
logger.error(f"Unauthorized access attempt with API Key({api_key}) for API({path})")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand Down
2 changes: 1 addition & 1 deletion src/framex/plugins/proxy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ async def call_proxy_function(self, func_name: str, data: str) -> str:
else:
kwargs = decode_kwargs
tag = "remote" if proxy_func.is_remote else "local"
logger.info(f"Calling proxy function[{tag}]: {decode_func_name}, kwargs: {decode_kwargs}")
logger.info(f"Calling proxy function[{tag}]: {decode_func_name}")
res = await proxy_func.func(**kwargs)
return res if proxy_func.is_remote else cache_encode(res)
raise RuntimeError(f"Proxy function({decode_func_name}) not registered")
4 changes: 2 additions & 2 deletions tests/api/test_echo.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ def test_echo(client: TestClient):
def test_echo_with_no_api_key(client: TestClient):
params = {"message": "hello world"}
res = client.get(f"{API_STR}/echo", params=params).json()
assert res["status"] == 403
assert res["message"] == "Not authenticated"
assert res["status"] == 401
assert res["message"] == "Invalid API Key(None) for API(/api/v1/echo)"


def test_echo_with_error_api_key(client: TestClient):
Expand Down
50 changes: 1 addition & 49 deletions tests/driver/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from starlette import status
from starlette.exceptions import HTTPException

from framex.consts import API_STR, DOCS_URL, OPENAPI_URL
from framex.consts import API_STR
from framex.driver.application import create_fastapi_application


Expand All @@ -33,45 +33,6 @@ def test_cors_middleware_configured(self):
assert "CORSMiddleware" in middleware_classes


class TestAuthenticationEndpoints:
@pytest.fixture
def app(self):
return create_fastapi_application()

@pytest.fixture
def client(self, app):
return TestClient(app)

def test_docs_requires_auth(self, client):
response = client.get(DOCS_URL)
assert response.status_code == status.HTTP_401_UNAUTHORIZED

def test_docs_wrong_credentials(self, client):
response = client.get(DOCS_URL, auth=("bad", "bad"))
assert response.status_code == status.HTTP_401_UNAUTHORIZED

def test_docs_correct_credentials(self, client):
from framex.config import settings

response = client.get(
DOCS_URL,
auth=(settings.server.docs_user, settings.server.docs_password),
)
assert response.status_code == status.HTTP_200_OK
assert "text/html" in response.headers["content-type"]

def test_openapi_correct_credentials(self, client):
from framex.config import settings

response = client.get(
OPENAPI_URL,
auth=(settings.server.docs_user, settings.server.docs_password),
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["info"]["title"] == "FrameX API"


class TestExceptionHandlers:
@pytest.fixture
def app(self):
Expand Down Expand Up @@ -146,15 +107,6 @@ async def endpoint() -> Any:
assert data["data"] == {"result": "ok"}
assert "timestamp" in data

def test_docs_not_wrapped(self, client):
from framex.config import settings

response = client.get(
DOCS_URL,
auth=(settings.server.docs_user, settings.server.docs_password),
)
assert "text/html" in response.headers["content-type"]


class TestLifespanBehavior:
@patch("framex.config.settings")
Expand Down
Loading
Loading