fix(csrf): supprimer prevent_initial_call=True sur _fill_csrf_inputs

Avec prevent_initial_call=True, le callback ne s'exécutait pas lors de
la chaîne initiale (_generate_csrf_token → csrf-token), laissant le champ
csrf_token vide au premier chargement direct de /connexion → erreur 400.

Ajoute des tests comportementaux avec CSRF activé (comme en production) et
un test architectural qui vérifie que le callback reste appelable initialement.
Corrige aussi les assertions de redirection post-login (/compte/abonnement).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Colin Maudry
2026-06-29 15:46:30 +02:00
parent f8112274cf
commit 8cd5bfe821
4 changed files with 91 additions and 3 deletions
+28
View File
@@ -33,6 +33,34 @@ def client(app):
return app.test_client()
@pytest.fixture
def csrf_app(users_db_path, monkeypatch):
"""App Flask avec protection CSRF activée, comme en production."""
from flask import Flask
from flask_wtf.csrf import generate_csrf
from src.auth.setup import init_auth
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.setenv("LINKEDIN_CLIENT_ID", "test-client-id")
monkeypatch.setenv("LINKEDIN_CLIENT_SECRET", "test-client-secret")
monkeypatch.setenv("APP_BASE_URL", "http://localhost:8050")
app = Flask(__name__)
init_auth(app)
@app.route("/_test/csrf")
def _test_csrf():
return generate_csrf()
yield app
@pytest.fixture
def csrf_client(csrf_app):
return csrf_app.test_client()
@pytest.fixture
def mail_outbox(app, monkeypatch):
from src.auth import mailer
+29 -2
View File
@@ -17,7 +17,7 @@ def test_login_success(client, users_db_path):
data={"email": "a@b.c", "password": "password12"},
)
assert resp.status_code == 302
assert resp.headers["Location"].endswith("/compte/admin")
assert resp.headers["Location"].endswith("/compte/abonnement")
def test_login_wrong_password(client, users_db_path):
@@ -63,7 +63,7 @@ def test_login_rejects_absolute_next(client, users_db_path):
"next": "https://evil.com",
},
)
assert resp.headers["Location"].endswith("/compte/admin")
assert resp.headers["Location"].endswith("/compte/abonnement")
def test_logout_clears_session(client, users_db_path):
@@ -72,3 +72,30 @@ def test_logout_clears_session(client, users_db_path):
resp = client.post("/auth/logout")
assert resp.status_code == 302
assert resp.headers["Location"].endswith("/")
# --- Tests CSRF (protection active, comme en production) ---
def test_login_rejects_missing_csrf_token(csrf_client):
"""POST /auth/login sans token CSRF → 400.
Régression : avec prevent_initial_call=True sur _fill_csrf_inputs, le token
n'était pas injecté dans le formulaire lors du chargement initial de /connexion.
"""
resp = csrf_client.post(
"/auth/login",
data={"email": "a@b.c", "password": "password12"},
)
assert resp.status_code == 400
def test_login_accepts_valid_csrf_token(csrf_client, users_db_path):
"""POST /auth/login avec token CSRF valide → 302."""
_make_verified_user()
token = csrf_client.get("/_test/csrf").data.decode()
resp = csrf_client.post(
"/auth/login",
data={"email": "a@b.c", "password": "password12", "csrf_token": token},
)
assert resp.status_code == 302