Files
benevoles/backend/tests/test_api_positions.py
AdamThomasandClaude Sonnet 5 8fcc22115f Gestion des référents depuis la fiche bénévole
Un poste avait bien un `lead_account_id`, lu par l'écran référent
(`/mobile-referent`) et par les droits d'écriture (`ensure_can_write_position`),
mais rien ne permettait de le renseigner : aucun écran ne l'exposait, et
l'API ne validait même pas que la valeur envoyée désignait un compte référent
existant de la bonne édition.

Backend :
- `PATCH /editions/{id}/accounts/{account_id}` (nouveau) : change le `role`
  d'un compte, seulement entre LEAD et VOLUNTEER (lien magique dans les deux
  cas) — 422 `account.role_change_not_allowed` dès que COORDINATOR est
  impliqué (mot de passe en jeu, jamais touché ici). C'est ce qui permet de
  faire passer le compte d'un bénévole en référent.
- `POST`/`PATCH /positions` : `lead_account_id` vérifié — doit désigner un
  compte `Role.LEAD` de la même édition, sinon 422
  `position.lead_must_be_referent_account` (silencieusement accepté
  jusqu'ici, y compris un compte d'une autre édition ou du mauvais rôle).

Frontend (`ConfigVolunteers.tsx`) : nouvelle section « Référent des postes »
sur la fiche du bénévole, juste après l'accès de connexion — mêmes badges à
cocher que les vœux de poste/artiste. Cocher un poste crée ou met à niveau
(LEAD) l'accès de connexion du bénévole automatiquement ; décocher retire
seulement le poste, sans jamais rétrograder l'accès. Un bénévole dont le
compte est déjà COORDINATOR ne peut pas être désigné référent (il a déjà
accès à tout). Miroir complet côté mock (`accounts.update`,
`checkLeadAccount`) pour que la démonstration se comporte pareil.

Vérifié : 204 tests backend pertinents (14 nouveaux : 6 sur `PATCH accounts`,
8 sur la validation `lead_account_id`), ruff, alembic check ; typecheck/
lint/build/smoke frontend ; parcours complet testé en direct sur le jeu de
démo (création de compte, promotion LEAD, rejet COORDINATOR, affectation
référent) avant nettoyage des données de test.

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

112 lines
3.8 KiB
Python

import uuid
from app.models import Role
def _url(edition, suffix=""):
return f"/editions/{edition.id}/positions{suffix}"
def test_position_crud_roundtrip(client, edition):
created = client.post(
_url(edition),
json={"name": "Billetterie", "category": "TICKETING", "location": "Entrée A"},
)
assert created.status_code == 201
pid = created.json()["id"]
assert created.json()["category"] == "TICKETING"
assert client.get(_url(edition, f"/{pid}")).json()["name"] == "Billetterie"
assert [p["name"] for p in client.get(_url(edition)).json()] == ["Billetterie"]
patched = client.patch(
_url(edition, f"/{pid}"), json={"location": "Entrée B"}
)
assert patched.json()["location"] == "Entrée B"
assert patched.json()["name"] == "Billetterie"
assert client.delete(_url(edition, f"/{pid}")).status_code == 204
assert client.get(_url(edition, f"/{pid}")).status_code == 404
def test_unknown_position_is_404(client, edition):
assert client.get(_url(edition, f"/{uuid.uuid4()}")).status_code == 404
def test_unknown_edition_is_404(client):
assert (
client.get(f"/editions/{uuid.uuid4()}/positions").status_code == 404
)
def test_sets_a_referent(client, edition, make_account):
lead = make_account(Role.LEAD, email="referent@ex.fr")
created = client.post(
_url(edition),
json={"name": "Bar", "category": "BAR"},
)
pid = created.json()["id"]
resp = client.patch(_url(edition, f"/{pid}"), json={"lead_account_id": str(lead.id)})
assert resp.status_code == 200
assert resp.json()["lead_account_id"] == str(lead.id)
def test_referent_must_have_the_lead_role(client, edition, make_account):
volunteer_account = make_account(Role.VOLUNTEER, email="v@ex.fr")
created = client.post(_url(edition), json={"name": "Bar", "category": "BAR"})
pid = created.json()["id"]
resp = client.patch(
_url(edition, f"/{pid}"), json={"lead_account_id": str(volunteer_account.id)}
)
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "position.lead_must_be_referent_account"
def test_referent_must_belong_to_the_same_edition(client, edition, session, make_account):
import datetime as dt
from app.models import Edition
other_edition = Edition(year=2027, start_date=dt.date(2027, 7, 1), end_date=dt.date(2027, 7, 4))
session.add(other_edition)
session.commit()
session.refresh(other_edition)
other_lead = make_account(Role.LEAD, email="stranger@ex.fr")
other_lead.edition_id = other_edition.id
session.add(other_lead)
session.commit()
created = client.post(_url(edition), json={"name": "Bar", "category": "BAR"})
pid = created.json()["id"]
resp = client.patch(
_url(edition, f"/{pid}"), json={"lead_account_id": str(other_lead.id)}
)
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "position.lead_must_be_referent_account"
def test_unknown_referent_is_422(client, edition):
created = client.post(_url(edition), json={"name": "Bar", "category": "BAR"})
pid = created.json()["id"]
resp = client.patch(
_url(edition, f"/{pid}"), json={"lead_account_id": str(uuid.uuid4())}
)
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "position.lead_must_be_referent_account"
def test_clearing_the_referent_is_allowed(client, edition, make_account):
lead = make_account(Role.LEAD, email="referent2@ex.fr")
created = client.post(_url(edition), json={"name": "Bar", "category": "BAR"})
pid = created.json()["id"]
client.patch(_url(edition, f"/{pid}"), json={"lead_account_id": str(lead.id)})
resp = client.patch(_url(edition, f"/{pid}"), json={"lead_account_id": None})
assert resp.status_code == 200
assert resp.json()["lead_account_id"] is None