"""Tests — cookies Secure, CORS same-origin explicite, avertissement bind (ROADMAP #87 T3/T8). - `is_secure_cookies()` : `OBSIGATE_SECURE_COOKIES=true|false|auto` (défaut `auto` : Secure si la requête arrive en https, sinon pas de flag — les navigateurs ignorent les cookies `Secure` en clair). - CORS same-origin EXPLICITE : `CORSMiddleware(allow_origins=[])` — aucun `Access-Control-Allow-*` n'est émis même avec un `Origin` cross-origin, et les preflights sont rejetés (400). """ from __future__ import annotations def test_secure_cookies_default_auto(monkeypatch): """Défaut `auto` : sans requête → pas de flag (logins HTTP locaux préservés).""" from backend.auth.router import is_secure_cookies monkeypatch.delenv("OBSIGATE_SECURE_COOKIES", raising=False) assert is_secure_cookies() is False def test_secure_cookies_forced_values(monkeypatch): """`true`/`1`/`yes` → flag ; `false`/`0`/`no` → pas de flag (insensible à la casse).""" from backend.auth.router import is_secure_cookies for value in ("true", "True", "TRUE", "1", "yes", "on"): monkeypatch.setenv("OBSIGATE_SECURE_COOKIES", value) assert is_secure_cookies() is True for value in ("false", "False", "FALSE", "0", "no", "off"): monkeypatch.setenv("OBSIGATE_SECURE_COOKIES", value) assert is_secure_cookies() is False def test_no_cors_headers_on_api(client): """Pas de `Access-Control-Allow-Origin` : same-origin imposé par le navigateur.""" resp = client.get("/api/health") assert resp.status_code == 200 assert "access-control-allow-origin" not in {k.lower() for k in resp.headers} def test_no_cors_headers_on_public_share(client): """Idem sur la page publique de partage.""" resp = client.get("/s/jeton-inexistant") assert resp.status_code == 404 assert "access-control-allow-origin" not in {k.lower() for k in resp.headers} def test_cross_origin_get_emits_no_acao(client): """#87 T8 : même avec un `Origin` cross-origin, aucun ACAO (refus explicite).""" resp = client.get("/api/health", headers={"Origin": "http://evil.example"}) assert resp.status_code == 200 assert "access-control-allow-origin" not in {k.lower() for k in resp.headers} def test_cross_origin_preflight_rejected(client): """#87 T8 : preflight cross-origin → 400 (origine non autorisée).""" resp = client.options( "/api/health", headers={ "Origin": "http://evil.example", "Access-Control-Request-Method": "GET", }, ) assert resp.status_code == 400 def test_security_headers_present(client): """En-têtes de durcissement posés par le middleware (non-régression).""" resp = client.get("/api/health") assert resp.headers.get("x-content-type-options") == "nosniff" assert resp.headers.get("x-frame-options") == "SAMEORIGIN" csp = resp.headers.get("content-security-policy", "") assert "object-src 'none'" in csp assert "frame-ancestors 'self'" in csp def _csp_directives(csp: str) -> dict[str, list[str]]: """Découpe une politique CSP en ``{directive: [sources]}``.""" directives: dict[str, list[str]] = {} for part in csp.split(";"): tokens = part.split() if tokens: directives[tokens[0]] = tokens[1:] return directives def test_csp_frame_src_allows_agent_console(client): """BUG-119 : le « mode console » du panneau Spark charge la console du service d'agents (``https://agent.dracodev.net``) dans une iframe — ``frame-src`` doit autoriser cette origine, sinon le navigateur bloque le chargement (« This content is blocked »). Les sources historiques (``'self'`` et ``blob:``, visualiseur PDF) restent en place et aucune autre origine n'est admise.""" resp = client.get("/api/health") csp = resp.headers.get("content-security-policy", "") directives = _csp_directives(csp) assert directives.get("frame-src") == [ "'self'", "blob:", "https://agent.dracodev.net", ]