102 lines
4.0 KiB
Python
102 lines
4.0 KiB
Python
"""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",
|
|
]
|