Files
benevoles/backend/tests/test_auth_password.py
AdamThomasandClaude Sonnet 5 8a879ca5c3 Étape 8 : connexion par mot de passe pour la coordination
Deux systèmes d'authentification, une même session JWT :
- COORDINATION : login e-mail + mot de passe (bcrypt), POST .../auth/login ;
  POST .../auth/change-password pour le compte courant
- RÉFÉRENTS / BÉNÉVOLES : lien magique inchangé, mais refusé pour un compte
  qui a un mot de passe

- Account.password_hash (nullable) + migration 0002 ; propriété auth_method
  ("password" / "magic_link") exposée dans AccountRead
- app/auth/passwords.py : hash/verify bcrypt, longueur mini 10
- Création de compte : password requis pour COORDINATOR, interdit sinon
- seed : la coordination de démo a un mot de passe, affiché dans le résumé
- 12 tests (login ok/ko, séparation des deux systèmes, change-password,
  validation à la création) ; 129 tests au total ; alembic check vert

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-09 23:42:31 +02:00

158 lines
4.9 KiB
Python

"""Connexion par mot de passe (admins) et séparation des deux systèmes."""
from sqlmodel import select
from app.models import MagicLinkToken, Role
PASSWORD = "coordination-2026"
def _url(edition, suffix):
return f"/editions/{edition.id}/auth/{suffix}"
def test_login_with_correct_password(raw_client, edition, make_account):
make_account(Role.COORDINATOR, email="admin@e.fr", password=PASSWORD)
resp = raw_client.post(
_url(edition, "login"),
json={"email": "ADMIN@e.fr", "password": PASSWORD},
)
assert resp.status_code == 200
body = resp.json()
assert body["token_type"] == "bearer"
assert body["account"]["role"] == "COORDINATOR"
assert body["account"]["auth_method"] == "password"
def test_login_rejects_wrong_password(raw_client, edition, make_account):
make_account(Role.COORDINATOR, email="admin@e.fr", password=PASSWORD)
resp = raw_client.post(
_url(edition, "login"),
json={"email": "admin@e.fr", "password": "faux"},
)
assert resp.status_code == 401
assert resp.json()["detail"]["code"] == "auth.invalid_credentials"
def test_login_rejects_unknown_email(raw_client, edition):
resp = raw_client.post(
_url(edition, "login"),
json={"email": "personne@e.fr", "password": PASSWORD},
)
assert resp.status_code == 401
def test_login_fails_for_magic_link_account(raw_client, edition, make_account):
make_account(Role.LEAD, email="ref@e.fr") # sans mot de passe
resp = raw_client.post(
_url(edition, "login"),
json={"email": "ref@e.fr", "password": "peu importe"},
)
assert resp.status_code == 401
def test_magic_link_ignored_for_password_account(
raw_client, session, edition, make_account
):
make_account(Role.COORDINATOR, email="admin@e.fr", password=PASSWORD)
resp = raw_client.post(
_url(edition, "magic-link"), json={"email": "admin@e.fr"}
)
assert resp.status_code == 202
assert session.exec(select(MagicLinkToken)).all() == []
def test_change_password(raw_client, edition, make_account):
make_account(Role.COORDINATOR, email="admin@e.fr", password=PASSWORD)
jwt = raw_client.post(
_url(edition, "login"),
json={"email": "admin@e.fr", "password": PASSWORD},
).json()["access_token"]
headers = {"Authorization": f"Bearer {jwt}"}
changed = raw_client.post(
_url(edition, "change-password"),
json={"current_password": PASSWORD, "new_password": "nouveau-mdp-2026"},
headers=headers,
)
assert changed.status_code == 204
assert (
raw_client.post(
_url(edition, "login"),
json={"email": "admin@e.fr", "password": PASSWORD},
).status_code
== 401
)
assert (
raw_client.post(
_url(edition, "login"),
json={"email": "admin@e.fr", "password": "nouveau-mdp-2026"},
).status_code
== 200
)
def test_change_password_rejects_wrong_current(
raw_client, edition, make_account
):
make_account(Role.COORDINATOR, email="admin@e.fr", password=PASSWORD)
jwt = raw_client.post(
_url(edition, "login"),
json={"email": "admin@e.fr", "password": PASSWORD},
).json()["access_token"]
resp = raw_client.post(
_url(edition, "change-password"),
json={"current_password": "faux", "new_password": "nouveau-mdp-2026"},
headers={"Authorization": f"Bearer {jwt}"},
)
assert resp.status_code == 401
# --------------------------------------------------------------------------- #
# Création de compte : mot de passe selon le rôle #
# --------------------------------------------------------------------------- #
def test_create_coordinator_requires_password(client, edition):
resp = client.post(
f"/editions/{edition.id}/accounts",
json={"email": "admin2@e.fr", "role": "COORDINATOR"},
)
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "account.password_required"
def test_create_coordinator_with_password_can_log_in(
client, raw_client, edition
):
created = client.post(
f"/editions/{edition.id}/accounts",
json={
"email": "admin2@e.fr",
"role": "COORDINATOR",
"password": "un-mot-de-passe-solide",
},
)
assert created.status_code == 201
assert created.json()["auth_method"] == "password"
assert (
raw_client.post(
f"/editions/{edition.id}/auth/login",
json={"email": "admin2@e.fr", "password": "un-mot-de-passe-solide"},
).status_code
== 200
)
def test_create_lead_rejects_password(client, edition):
resp = client.post(
f"/editions/{edition.id}/accounts",
json={
"email": "ref2@e.fr",
"role": "LEAD",
"password": "pas-permis",
},
)
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "account.password_not_allowed"