Files
benevoles/backend/tests/test_badges_api.py
T
AdamThomasandClaude Opus 5.5 cfed76c239 Badges : texte libre saisi dans l'éditeur, champs ramenés dans le badge
Texte libre commun à tous les badges, saisi dans l'éditeur
(BadgeField.text, migration 0025) ; le « Texte du badge » de la fiche le
remplace pour un bénévole. Champs ramenés à l'intérieur du badge et champ
sélectionné au premier plan : la photo restait bloquée hors d'un badge
plus étroit que le CR80 (bornes de glisser-déposer négatives).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:30:34 +01:00

250 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Configuration et génération des planches de badges (API)."""
import base64
import pytest
from app.models import Role
from tests.factories import dt_at, make_position, make_shift, make_volunteer
# PNG 1×1 valide (transparent), le plus petit possible.
_PNG = base64.b64decode(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY"
"42YAAAAASUVORK5CYII="
)
def _badges_url(edition):
return f"/editions/{edition.id}/badges"
def _set_full_name_field(client, edition):
return client.put(
f"{_badges_url(edition)}/template/fields",
json={
"fields": [
{
"field_type": "FULL_NAME",
"x_mm": 5,
"y_mm": 5,
"width_mm": 60,
"height_mm": 10,
"font_size": 12,
}
]
},
)
def test_template_is_coordinator_only(edition, make_account, as_account):
lead = make_account(Role.LEAD, email="lead@e.fr")
client = as_account(lead)
assert client.get(f"{_badges_url(edition)}/template").status_code == 403
resp = client.put(f"{_badges_url(edition)}/template/fields", json={"fields": []})
assert resp.status_code == 403
def test_template_starts_empty(client, edition):
resp = client.get(f"{_badges_url(edition)}/template")
assert resp.status_code == 200
body = resp.json()
assert body["has_front_image"] is False
assert body["has_back_image"] is False
assert body["fields"] == []
# Format CR80 par défaut tant qu'aucune image n'a été importée.
assert body["badge_width_mm"] == 85.6
assert body["badge_height_mm"] == 54.0
def test_uploading_front_image_derives_the_badge_format_from_its_pixels(client, edition):
resp = client.put(
f"{_badges_url(edition)}/template/front-image",
files={"file": ("front.png", _PNG, "image/png")},
)
body = resp.json()
# PNG 1×1 px, résolution supposée 300 dpi ⇒ 25,4/300 mm de côté.
assert body["badge_width_mm"] == pytest.approx(25.4 / 300)
assert body["badge_height_mm"] == pytest.approx(25.4 / 300)
def test_uploading_front_and_back_images(client, edition):
front = client.put(
f"{_badges_url(edition)}/template/front-image",
files={"file": ("front.png", _PNG, "image/png")},
)
assert front.status_code == 200
assert front.json()["has_front_image"] is True
back = client.put(
f"{_badges_url(edition)}/template/back-image",
files={"file": ("back.png", _PNG, "image/png")},
)
assert back.status_code == 200
assert back.json()["has_back_image"] is True
served = client.get(f"{_badges_url(edition)}/template/front-image")
assert served.status_code == 200
assert served.headers["content-type"] == "image/png"
assert served.content == _PNG
def test_uploading_an_unsupported_image_type_is_rejected(client, edition):
resp = client.put(
f"{_badges_url(edition)}/template/front-image",
files={"file": ("front.gif", b"not-a-real-gif", "image/gif")},
)
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "badge.image_type_not_supported"
def test_resizing_the_badge_scales_it_independently_of_the_image(client, edition):
client.put(
f"{_badges_url(edition)}/template/front-image",
files={"file": ("front.png", _PNG, "image/png")},
)
# Image 1×1 px à 300 dpi supposés ⇒ badge minuscule (~0,08 mm) : on
# l'agrandit à un format exploitable, en conservant le ratio (ici 1:1).
resp = client.put(
f"{_badges_url(edition)}/template/size",
json={"badge_width_mm": 90, "badge_height_mm": 90},
)
assert resp.status_code == 200
body = resp.json()
assert body["badge_width_mm"] == 90
assert body["badge_height_mm"] == 90
# Persisté : une relecture du gabarit renvoie toujours la taille agrandie.
again = client.get(f"{_badges_url(edition)}/template").json()
assert again["badge_width_mm"] == 90
assert again["badge_height_mm"] == 90
def test_resizing_the_badge_to_a_non_positive_size_is_rejected(client, edition):
resp = client.put(
f"{_badges_url(edition)}/template/size",
json={"badge_width_mm": 0, "badge_height_mm": 54},
)
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "badge.invalid_size"
def test_setting_the_same_field_type_twice_is_rejected(client, edition):
resp = client.put(
f"{_badges_url(edition)}/template/fields",
json={
"fields": [
{
"field_type": "FULL_NAME", "x_mm": 0, "y_mm": 0,
"width_mm": 10, "height_mm": 10,
},
{
"field_type": "FULL_NAME", "x_mm": 5, "y_mm": 5,
"width_mm": 10, "height_mm": 10,
},
]
},
)
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "badge.duplicate_field_type"
def test_generating_without_any_field_configured_is_rejected(client, edition, session):
make_volunteer(session, edition, first_name="Alex", last_name="Martin")
resp = client.post(f"{_badges_url(edition)}/generate", json={})
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "badge.no_fields"
def test_generating_with_no_matching_volunteers_is_rejected(client, edition, session):
_set_full_name_field(client, edition)
make_volunteer(session, edition, first_name="Alex", last_name="Martin")
resp = client.post(
f"{_badges_url(edition)}/generate",
json={"volunteer_ids": ["00000000-0000-0000-0000-000000000000"]},
)
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "badge.no_volunteers"
def test_generating_produces_a_pdf(client, edition, session):
from app.models import Assignment
resp = client.put(
f"{_badges_url(edition)}/template/fields",
json={
"fields": [
{
"field_type": "FULL_NAME", "x_mm": 5, "y_mm": 5,
"width_mm": 60, "height_mm": 10, "font_size": 12,
},
{
"field_type": "POSITIONS", "x_mm": 5, "y_mm": 20,
"width_mm": 60, "height_mm": 10, "font_size": 9,
},
]
},
)
assert resp.status_code == 200
client.put(
f"{_badges_url(edition)}/template/front-image",
files={"file": ("front.png", _PNG, "image/png")},
)
client.put(
f"{_badges_url(edition)}/template/back-image",
files={"file": ("back.png", _PNG, "image/png")},
)
volunteer = make_volunteer(session, edition, first_name="Alex", last_name="Martin")
position = make_position(session, edition, name="Bar central")
shift = make_shift(session, edition, position, start=dt_at(9, 18), end=dt_at(9, 22))
session.add(Assignment(edition_id=edition.id, volunteer_id=volunteer.id, shift_id=shift.id))
session.commit()
resp = client.post(
f"{_badges_url(edition)}/generate",
json={"volunteer_ids": [str(volunteer.id)]},
)
assert resp.status_code == 200
assert resp.headers["content-type"] == "application/pdf"
assert resp.content.startswith(b"%PDF")
def test_generating_with_photo_and_free_text_fields(client, edition, session):
resp = client.put(
f"{_badges_url(edition)}/template/fields",
json={
"fields": [
{
"field_type": "PHOTO", "x_mm": 60, "y_mm": 5,
"width_mm": 20, "height_mm": 25, "font_size": 8,
},
{
"field_type": "FREE_TEXT", "x_mm": 5, "y_mm": 40,
"width_mm": 50, "height_mm": 8, "font_size": 9,
"text": "Staff 2026",
},
]
},
)
assert resp.status_code == 200
by_type = {f["field_type"]: f for f in resp.json()}
assert set(by_type) == {"PHOTO", "FREE_TEXT"}
assert by_type["FREE_TEXT"]["text"] == "Staff 2026"
with_photo = make_volunteer(session, edition, first_name="Alex", last_name="Martin")
with_photo.photo = _PNG
with_photo.photo_content_type = "image/png"
without_photo = make_volunteer(session, edition, first_name="Sam", last_name="Nuit")
session.add(with_photo)
session.commit()
resp = client.patch(
f"/editions/{edition.id}/volunteers/{without_photo.id}",
json={"badge_text": "Accès backstage"},
)
assert resp.status_code == 200
assert resp.json()["badge_text"] == "Accès backstage"
resp = client.post(f"{_badges_url(edition)}/generate", json={})
assert resp.status_code == 200
assert resp.content.startswith(b"%PDF")