deploy: sync backend to Space root (learn-lesson HF cache fix)
Browse files- app/core/config.py +5 -6
- app/routes/auth.py +10 -4
- app/routes/chat.py +0 -0
- tests/test_media_auth_cookie.py +52 -0
app/core/config.py
CHANGED
|
@@ -13,12 +13,6 @@ class Settings(BaseSettings):
|
|
| 13 |
app_name: str = "AI Exam Success API"
|
| 14 |
app_version: str = "0.1.0"
|
| 15 |
environment: str = "development"
|
| 16 |
-
|
| 17 |
-
@property
|
| 18 |
-
def is_production(self) -> bool:
|
| 19 |
-
"""Single source for production-only cookie and security behaviour."""
|
| 20 |
-
return self.environment.strip().casefold() == "production"
|
| 21 |
-
|
| 22 |
database_url: str = f"sqlite:///{BACKEND_DIR / 'exam_success_dev.db'}"
|
| 23 |
# Dev defaults stay small; production .env should raise these for 1k+ concurrent students.
|
| 24 |
database_pool_size: int = Field(
|
|
@@ -310,6 +304,11 @@ class Settings(BaseSettings):
|
|
| 310 |
extra="ignore",
|
| 311 |
)
|
| 312 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
@property
|
| 314 |
def cors_origin_list(self) -> list[str]:
|
| 315 |
origins = [origin.strip() for origin in self.cors_origins.split(",") if origin.strip()]
|
|
|
|
| 13 |
app_name: str = "AI Exam Success API"
|
| 14 |
app_version: str = "0.1.0"
|
| 15 |
environment: str = "development"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
database_url: str = f"sqlite:///{BACKEND_DIR / 'exam_success_dev.db'}"
|
| 17 |
# Dev defaults stay small; production .env should raise these for 1k+ concurrent students.
|
| 18 |
database_pool_size: int = Field(
|
|
|
|
| 304 |
extra="ignore",
|
| 305 |
)
|
| 306 |
|
| 307 |
+
@property
|
| 308 |
+
def is_production(self) -> bool:
|
| 309 |
+
"""True when ENVIRONMENT is production (case-insensitive)."""
|
| 310 |
+
return self.environment.strip().casefold() == "production"
|
| 311 |
+
|
| 312 |
@property
|
| 313 |
def cors_origin_list(self) -> list[str]:
|
| 314 |
origins = [origin.strip() for origin in self.cors_origins.split(",") if origin.strip()]
|
app/routes/auth.py
CHANGED
|
@@ -267,12 +267,18 @@ def _auth_redirect_for_user(user: User, next_path: str) -> RedirectResponse:
|
|
| 267 |
|
| 268 |
|
| 269 |
def _is_production_environment(settings: object | None = None) -> bool:
|
| 270 |
-
"""Resolve production
|
| 271 |
active = settings or get_settings()
|
| 272 |
-
|
|
|
|
|
|
|
|
|
|
| 273 |
if isinstance(flag, bool):
|
| 274 |
-
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
| 276 |
|
| 277 |
|
| 278 |
def _set_media_auth_cookie(
|
|
|
|
| 267 |
|
| 268 |
|
| 269 |
def _is_production_environment(settings: object | None = None) -> bool:
|
| 270 |
+
"""Resolve production even if an older Settings build lacks is_production."""
|
| 271 |
active = settings or get_settings()
|
| 272 |
+
try:
|
| 273 |
+
flag = getattr(active, "is_production", None)
|
| 274 |
+
except AttributeError:
|
| 275 |
+
flag = None
|
| 276 |
if isinstance(flag, bool):
|
| 277 |
+
production = flag
|
| 278 |
+
else:
|
| 279 |
+
production = str(getattr(active, "environment", "") or "").strip().casefold() == "production"
|
| 280 |
+
logger.debug("[FIX] media-auth cookie production=%s", production)
|
| 281 |
+
return production
|
| 282 |
|
| 283 |
|
| 284 |
def _set_media_auth_cookie(
|
app/routes/chat.py
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tests/test_media_auth_cookie.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Regression tests for signup media-cookie setup.
|
| 2 |
+
|
| 3 |
+
CI failed because `_set_media_auth_cookie` read `settings.is_production`
|
| 4 |
+
while `Settings` only exposed `environment`.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from types import SimpleNamespace
|
| 10 |
+
|
| 11 |
+
from fastapi import Response
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def test_settings_exposes_is_production_from_environment() -> None:
|
| 15 |
+
from app.core.config import Settings
|
| 16 |
+
|
| 17 |
+
assert Settings(environment="production").is_production is True
|
| 18 |
+
assert Settings(environment="PRODUCTION").is_production is True
|
| 19 |
+
assert Settings(environment="development").is_production is False
|
| 20 |
+
assert Settings(environment=" staging ").is_production is False
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def test_signup_sets_media_cookie_without_crashing(auth_client) -> None:
|
| 24 |
+
response = auth_client.post(
|
| 25 |
+
"/auth/signup",
|
| 26 |
+
json={
|
| 27 |
+
"name": "Cookie Student",
|
| 28 |
+
"email": "media-cookie-fix@example.test",
|
| 29 |
+
"password": "Pass123!beta",
|
| 30 |
+
},
|
| 31 |
+
)
|
| 32 |
+
assert response.status_code == 201, response.text
|
| 33 |
+
assert response.json()["access_token"]
|
| 34 |
+
cookie = response.headers.get("set-cookie", "")
|
| 35 |
+
assert "docdoe_media_token=" in cookie
|
| 36 |
+
assert "secure" not in cookie.lower()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_media_cookie_is_secure_when_environment_is_production() -> None:
|
| 40 |
+
from app.routes.auth import _set_media_auth_cookie
|
| 41 |
+
|
| 42 |
+
response = Response()
|
| 43 |
+
_set_media_auth_cookie(
|
| 44 |
+
response,
|
| 45 |
+
"token-value",
|
| 46 |
+
3600,
|
| 47 |
+
settings=SimpleNamespace(environment="production"),
|
| 48 |
+
)
|
| 49 |
+
cookie = response.headers.get("set-cookie", "")
|
| 50 |
+
assert "docdoe_media_token=token-value" in cookie
|
| 51 |
+
assert "samesite=none" in cookie.lower()
|
| 52 |
+
assert "secure" in cookie.lower()
|