Files
benevoles/backend/tests/test_import_api.py
T
AdamThomasandClaude Sonnet 5 bb919554ff Étape 3 : import CSV/Excel des bénévoles et disponibilités
- Fichier unique, processus en deux temps : POST /editions/{id}/import/analyze
  (colonnes détectées, aperçu, mapping suggéré, demi-journées de l'édition)
  puis POST /editions/{id}/import/execute (mapping validé)
- Parser CSV (délimiteur et encodage devinés, BOM) et XLSX (openpyxl),
  détection XLSX par nombre magique
- Vocabulaire des valeurs de disponibilité (tri-état, accents/casse) et des
  tailles de t-shirt ; compétences créées à la volée dans le référentiel
- Rapport ligne par ligne : codes de traduction + paramètres, jamais de
  phrase. Doublons : upsert sur email, signalement sur (nom, prénom) sans
  email, doublons intra-fichier. Mode simulation (rollback, rien de persisté)
- 40 tests : parser, valeurs, service (tous les cas), endpoints
- Dépendances : python-multipart, openpyxl

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

120 lines
3.6 KiB
Python

import json
import uuid
from sqlmodel import select
from app.models import Volunteer
_CSV = (
"Prénom;Nom;Email;Compétences;Ven matin\n"
"Alex;Martin;alex@ex.fr;SST, Permis B;x\n"
"Sam;Nguyen;sam@ex.fr;SST;si besoin\n"
).encode()
def _mapping(dry_run: bool) -> str:
return json.dumps(
{
"dry_run": dry_run,
"columns": {
"first_name": "Prénom",
"last_name": "Nom",
"email": "Email",
"skills": "Compétences",
},
"availability_columns": [
{
"header": "Ven matin",
"date": "2026-07-10",
"day_part_code": "MORNING",
}
],
}
)
def test_analyze_returns_headers_preview_and_day_parts(client, edition):
resp = client.post(
f"/editions/{edition.id}/import/analyze",
files={"file": ("benevoles.csv", _CSV, "text/csv")},
)
assert resp.status_code == 200
body = resp.json()
assert body["format"] == "csv"
assert body["headers"] == ["Prénom", "Nom", "Email", "Compétences", "Ven matin"]
assert body["row_count"] == 2
assert body["preview"][0]["values"]["Nom"] == "Martin"
assert body["suggested_mapping"]["first_name"] == "Prénom"
assert {d["code"] for d in body["day_parts"]} == {
"MORNING",
"AFTERNOON",
"EVENING",
}
assert body["edition_dates"][0] == "2026-07-09"
def test_execute_dry_run_then_real(client, session, edition):
dry = client.post(
f"/editions/{edition.id}/import/execute",
files={"file": ("benevoles.csv", _CSV, "text/csv")},
data={"mapping": _mapping(dry_run=True)},
)
assert dry.status_code == 200
assert dry.json()["created"] == 2
assert session.exec(select(Volunteer)).all() == []
real = client.post(
f"/editions/{edition.id}/import/execute",
files={"file": ("benevoles.csv", _CSV, "text/csv")},
data={"mapping": _mapping(dry_run=False)},
)
assert real.status_code == 200
report = real.json()
assert report["created"] == 2
assert sorted(report["skills_created"]) == ["PERMIS_B", "SST"]
assert len(session.exec(select(Volunteer)).all()) == 2
def test_execute_rejects_invalid_mapping(client, edition):
bad_mapping = json.dumps(
{
"dry_run": True,
"columns": {"first_name": "Prénom", "last_name": "Nom"},
"availability_columns": [
{
"header": "Ven matin",
"date": "2026-07-10",
"day_part_code": "NIGHT",
}
],
}
)
resp = client.post(
f"/editions/{edition.id}/import/execute",
files={"file": ("benevoles.csv", _CSV, "text/csv")},
data={"mapping": bad_mapping},
)
assert resp.status_code == 422
detail = resp.json()["detail"]
assert detail["code"] == "import.mapping.invalid"
assert any(
e["code"] == "import.mapping.unknown_day_part" for e in detail["errors"]
)
def test_analyze_unknown_edition_is_404(client):
resp = client.post(
f"/editions/{uuid.uuid4()}/import/analyze",
files={"file": ("benevoles.csv", _CSV, "text/csv")},
)
assert resp.status_code == 404
def test_analyze_rejects_empty_file(client, edition):
resp = client.post(
f"/editions/{edition.id}/import/analyze",
files={"file": ("benevoles.csv", b"", "text/csv")},
)
assert resp.status_code == 400
assert resp.json()["detail"]["code"] == "import.error.empty_file"