test(cors): /api/search un seul jeu d'en-tetes CORS, origine refusee inchangee (defaut 3b)
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (pull_request) Failing after 1m49s
FlowDeck CI / test (pull_request) Failing after 32m50s
FlowDeck CI / docker (pull_request) Skipped
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (pull_request) Failing after 1m49s
FlowDeck CI / test (pull_request) Failing after 32m50s
FlowDeck CI / docker (pull_request) Skipped
This commit is contained in:
@@ -0,0 +1,120 @@
|
|||||||
|
"""Défaut 3b — /api/search Flowdeck depuis la console agent-dev.
|
||||||
|
|
||||||
|
Cause prouvée en déployé (flowdeck-dev, 2026-10-28) : sur une réponse
|
||||||
|
GET /api/search, le CORSMiddleware global pose déjà
|
||||||
|
``Access-Control-Allow-Credentials: true`` (comportement Starlette
|
||||||
|
avec ``allow_credentials=True``, même hors allowlist), et l'ancien
|
||||||
|
middleware de recherche EMPILAIT ses en-têtes par-dessus — d'où, pour
|
||||||
|
l'origine agent-dev, ``Allow-Credentials`` en double (« true, true »
|
||||||
|
≠ « true » pour le contrôle CORS du navigateur → fetch rejeté →
|
||||||
|
« injoignable (CORS ou réseau) » dans la console, alors que TikTak et
|
||||||
|
Kanbante, sans ce cumul, répondent avec des en-têtes uniques) et,
|
||||||
|
pour une origine sœur déjà dans la allowlist globale, ``Allow-Origin``
|
||||||
|
en double (rejet certain). Le correctif : le middleware de recherche
|
||||||
|
REMPLACE les en-têtes CORS existants au lieu de les empiler (même
|
||||||
|
patron que app/recherche_cors.py de la console).
|
||||||
|
|
||||||
|
Ces tests pilotent le middleware en ASGI pur, avec une application
|
||||||
|
interne qui imite le CORSMiddleware global (un en-tête
|
||||||
|
Allow-Credentials déjà posé, et, pour une sœur, Allow-Origin aussi).
|
||||||
|
``sister_origins`` est figé sur les listes officielles (les fonctions
|
||||||
|
pures sont déjà couvertes par tests/test_recherche.py) pour garder le
|
||||||
|
test hermétique (aucune résolution draco_urls, aucune base).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.middleware import recherche_cors as rc
|
||||||
|
|
||||||
|
ORIGINES = rc.PROD_ORIGINS | rc.DEV_ORIGINS | rc.AGENT_ORIGINS
|
||||||
|
|
||||||
|
|
||||||
|
def _lancer(middleware, scope):
|
||||||
|
"""Exécute le middleware ASGI et retourne les messages envoyés."""
|
||||||
|
recus = []
|
||||||
|
|
||||||
|
async def recevoir():
|
||||||
|
return {"type": "http.request", "body": b"", "more_body": False}
|
||||||
|
|
||||||
|
async def envoyer(message):
|
||||||
|
recus.append(message)
|
||||||
|
|
||||||
|
asyncio.run(middleware(scope, recevoir, envoyer))
|
||||||
|
return recus
|
||||||
|
|
||||||
|
|
||||||
|
def _application_interne(entetes):
|
||||||
|
async def appli(scope, recevoir, envoyer):
|
||||||
|
await envoyer({"type": "http.response.start", "status": 401,
|
||||||
|
"headers": list(entetes)})
|
||||||
|
await envoyer({"type": "http.response.body", "body": b"{}"})
|
||||||
|
return appli
|
||||||
|
|
||||||
|
|
||||||
|
def _scope(methode, origine):
|
||||||
|
return {"type": "http", "method": methode, "path": "/api/search",
|
||||||
|
"headers": [(b"origin", origine.encode("latin-1")),
|
||||||
|
(b"host", b"flowdeck-dev.dracodev.net")]}
|
||||||
|
|
||||||
|
|
||||||
|
def _compter(entetes, nom):
|
||||||
|
return sum(1 for k, _v in entetes if k == nom.encode("latin-1"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _origines_figees(monkeypatch):
|
||||||
|
monkeypatch.setattr(rc, "sister_origins", lambda *a, **k: ORIGINES)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_agent_dev_un_seul_jeu_d_entetes():
|
||||||
|
"""Cas du défaut : globale (ACAC seul) + recherche → un seul jeu."""
|
||||||
|
interne = _application_interne([
|
||||||
|
(b"content-type", b"application/json"),
|
||||||
|
(b"access-control-allow-credentials", b"true")])
|
||||||
|
recus = _lancer(rc.RechercheCorsMiddleware(interne),
|
||||||
|
_scope("GET", "https://agent-dev.dracodev.net"))
|
||||||
|
entetes = recus[0]["headers"]
|
||||||
|
assert _compter(entetes, "access-control-allow-credentials") == 1
|
||||||
|
assert _compter(entetes, "access-control-allow-origin") == 1
|
||||||
|
assert _compter(entetes, "vary") == 1
|
||||||
|
assert (b"access-control-allow-origin",
|
||||||
|
b"https://agent-dev.dracodev.net") in entetes
|
||||||
|
assert (b"content-type", b"application/json") in entetes
|
||||||
|
assert recus[0]["status"] == 401 # le statut traverse inchangé
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_origine_soeur_pas_d_allow_origin_double():
|
||||||
|
"""Sœur déjà dans la allowlist globale : pas de doublon non plus."""
|
||||||
|
interne = _application_interne([
|
||||||
|
(b"access-control-allow-origin", b"https://og-dev.dracodev.net"),
|
||||||
|
(b"access-control-allow-credentials", b"true"),
|
||||||
|
(b"vary", b"Origin")])
|
||||||
|
recus = _lancer(rc.RechercheCorsMiddleware(interne),
|
||||||
|
_scope("GET", "https://og-dev.dracodev.net"))
|
||||||
|
entetes = recus[0]["headers"]
|
||||||
|
assert _compter(entetes, "access-control-allow-origin") == 1
|
||||||
|
assert _compter(entetes, "access-control-allow-credentials") == 1
|
||||||
|
assert _compter(entetes, "vary") == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_origine_non_autorisee_traverse_sans_modification():
|
||||||
|
"""Une origine refusée ne reçoit aucun Allow-Origin ajouté."""
|
||||||
|
interne = _application_interne([
|
||||||
|
(b"access-control-allow-credentials", b"true")])
|
||||||
|
recus = _lancer(rc.RechercheCorsMiddleware(interne),
|
||||||
|
_scope("GET", "https://example.com"))
|
||||||
|
entetes = recus[0]["headers"]
|
||||||
|
assert _compter(entetes, "access-control-allow-origin") == 0
|
||||||
|
assert entetes == [(b"access-control-allow-credentials", b"true")]
|
||||||
|
|
||||||
|
|
||||||
|
def test_preflight_agent_dev_toujours_unique():
|
||||||
|
"""Le preflight (court-circuit avant l'interne) reste propre."""
|
||||||
|
recus = _lancer(rc.RechercheCorsMiddleware(_application_interne([])),
|
||||||
|
_scope("OPTIONS", "https://agent-dev.dracodev.net"))
|
||||||
|
entetes = recus[0]["headers"]
|
||||||
|
assert recus[0]["status"] == 204
|
||||||
|
assert _compter(entetes, "access-control-allow-origin") == 1
|
||||||
|
assert _compter(entetes, "access-control-allow-credentials") == 1
|
||||||
Reference in New Issue
Block a user