fix: A26 + A33 + A34 + A35 + A36 + A43 — bande S/XS du P2 (v7.3.9)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s

- A26 — champ `standalone` mort supprimé (0 lecteur) ; `sqlite:////data/…` ne
  produit plus de chemin UNC sous Windows ; `.env.example` ne promet plus
  PostgreSQL ; raise au boot si `APP_SECRET_KEY` vaut la valeur par défaut
- A33 — rate limit : préfixes manquants (`/scim/v2/`, `/workspace/`, `/db/` +
  non-GET sur `/s/` et `/f/`), limite lue dans `settings.rate_limit_requests`
  (60 annoncés / 100 codés en dur), clé `X-Forwarded-For` seulement derrière un
  proxy local (anti-spoof), `_store` épuré (mémoire bornée) + test dédié
- A34 — `_spawn()` : les 10 schedulers loggent leur exception et redémarrent
  après 10 s au lieu de mourir en silence ; 2 `logger.debug` de scheduler → warning
- A35 — OpenAPI régénéré 439 → 511 chemins (`info.version 7.3.9`), README
  (était v6.7.0), compteur API_GUIDE, titre dupliqué retiré du ROADMAP ;
  le drift Python 3.12/3.13 reste noté (rebuild d'image à valider)
- A36 — 4 dépendances mortes purgées de requirements.txt (aiosqlite, slowapi,
  loguru, packaging = 0 import) ; pyproject reste sans [project] : Docker et la
  CI installent requirements.txt, dupliquer les 22 deps créerait 2 sources
- A43 — 15 `datetime.utcnow()` → `now(UTC).replace(tzinfo=None)` (format ISO
  naïf identique, warnings de dépréciation divisés : 2374 → 926)

suite **1029/1029** · `ruff check app tests` OK · docs/ROADMAP/CHANGELOG/WORKLOAD à jour
This commit is contained in:
2026-10-01 08:50:39 -04:00
parent ffa1fa89ab
commit cb47f5c7f4
25 changed files with 3289 additions and 109 deletions
+29
View File
@@ -36,6 +36,35 @@ def test_public_view_hides_restricted_collection(client):
assert "Internal" not in r.text
def test_rate_limit_key_and_prune():
"""A33 : XFF ignoré depuis une IP publique (anti-bypass), épurage du store."""
from types import SimpleNamespace
from app.middleware.security import RateLimitMiddleware
mw = RateLimitMiddleware(None)
def req(host, fwd=None):
headers = {"x-forwarded-for": fwd} if fwd else {}
return SimpleNamespace(headers=headers, client=SimpleNamespace(host=host))
# IP publique (globale) : le client peut spoofer XFF autant qu'il veut → clé d'origine
assert mw._client_key(req("8.8.8.8", "1.2.3.4")) == "8.8.8.8"
# Derrière un proxy local : on prend le premier hop XFF
assert mw._client_key(req("10.0.0.1", "198.51.100.7, 10.0.0.2")) == "198.51.100.7"
# Sans XFF / hôte non IP (testserver)
assert mw._client_key(req("testserver")) == "testserver"
# Épurage : les fenêtres expirées sortent du store
import time
now = time.time()
mw._store["old"] = (now - 3600, 5)
mw._store["fresh"] = (now, 1)
mw._prune(now)
assert "old" not in mw._store and "fresh" in mw._store
def test_no_duplicate_routes():
"""A24 : deux routes même méthode+chemin → l'une écrase silencieusement l'autre."""
from app.main import app
+2 -2
View File
@@ -217,8 +217,8 @@ class TestV54TrashPurge:
with get_conn() as conn:
# insert 2 deleted pages: one old, one recent
old = datetime.datetime.utcnow() - datetime.timedelta(days=45)
recent = datetime.datetime.utcnow() - datetime.timedelta(days=5)
old = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - datetime.timedelta(days=45)
recent = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - datetime.timedelta(days=5)
cur = conn.execute(
"INSERT INTO pages (workspace, title, deleted_at) VALUES (?,?,?)",
("test", "Old Page", old.isoformat()),