Inscription en deux temps, emails de confirmation et export Excel des bénévoles
ci-cd / build-backend (push) Successful in 30s
ci-cd / build-frontend (push) Successful in 18s
ci-cd / deploy (push) Successful in 12s

Formulaire public — paiement en ligne :
- Le bénévole n'est plus compté en base tant qu'un paiement en ligne choisi
  n'est pas confirmé (PendingFormSubmission, promu en Volunteer à la
  confirmation, webhook ou vérification en direct).
- Type de paiement « mécénat » : étape d'adresse postale avant le paiement,
  conservée sur le bénévole pour un reçu fiscal (cerfa) ultérieur.
- Page de confirmation au retour de SumUp, avec relance possible après
  échec/expiration sans ressaisir le formulaire.
- « Je paierai sur place » remplace « je ne participe pas » ; tous les
  champs activés par la coordination sont désormais obligatoires.
- La fiche bénévole n'affiche plus l'état des cases à cocher, seulement le
  résultat réel du paiement (libellé, montant, statut).

Emails :
- Email de confirmation d'inscription à la création du bénévole (tient lieu
  de reçu de paiement quand une cotisation est payée en ligne).
- Mise en forme HTML au thème de l'application (logo de l'édition en
  en-tête) pour tous les emails envoyés, texte brut conservé en repli.

Export Excel des bénévoles (COORDINATION) : coordonnées, vœux résolus en
noms lisibles, cotisation et mécénat.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-09-30 07:41:58 +02:00
co-authored by Claude Sonnet 5
parent 68d42ee303
commit a2b748566b
34 changed files with 2318 additions and 291 deletions
+10
View File
@@ -0,0 +1,10 @@
"""Aide de test : les messages capturés par `outbox` (conftest.py) sont
`multipart/alternative` depuis l'ajout d'une version HTML (voir
`app/email/sender.py:build_message`) — `.get_content()` ne suffit plus à lui
seul, il faut préciser quelle partie lire."""
from email.message import EmailMessage
def plain_text(message: EmailMessage) -> str:
return message.get_body(preferencelist=("plain",)).get_content()
+5 -4
View File
@@ -2,6 +2,7 @@ from sqlmodel import select
from app.models import Assignment, MagicLinkToken, Role
from tests.factories import dt_at, make_position, make_shift, make_volunteer
from tests.mail import plain_text
def _url(edition):
@@ -38,17 +39,17 @@ def test_one_email_per_volunteer_with_an_address(client, edition, session, outbo
by_to = {m["To"]: m for m in outbox}
assert by_to["ana@e.fr"]["Subject"] == "Planning de Ana"
ana_body = by_to["ana@e.fr"].get_content()
ana_body = plain_text(by_to["ana@e.fr"])
assert "Tu as 2 créneau(x), 7.0 h." in ana_body
assert "Bar" in ana_body
assert "Tu as 0 créneau(x), 0.0 h." in by_to["bob@e.fr"].get_content()
assert "Tu as 0 créneau(x), 0.0 h." in plain_text(by_to["bob@e.fr"])
def test_default_template_is_used_when_body_omitted(client, edition, session, outbox):
make_volunteer(session, edition, first_name="Ana", email="ana@e.fr")
resp = client.post(_url(edition), json={})
assert resp.status_code == 200
assert outbox[0].get_content().startswith("Bonjour Ana,")
assert plain_text(outbox[0]).startswith("Bonjour Ana,")
def test_magic_link_is_embedded_when_placeholder_present(
@@ -61,7 +62,7 @@ def test_magic_link_is_embedded_when_placeholder_present(
_url(edition), json={"body": "Connecte-toi : {lien_connexion}"}
)
assert resp.status_code == 200
assert "/connexion?token=" in outbox[0].get_content()
assert "/connexion?token=" in plain_text(outbox[0])
assert len(session.exec(select(MagicLinkToken)).all()) == 1
+41
View File
@@ -1,6 +1,8 @@
import datetime as dt
import io
import uuid
from openpyxl import load_workbook
from sqlmodel import select
from app.models import Assignment, Availability, Role, Volunteer
@@ -389,3 +391,42 @@ def test_lead_can_read_any_volunteers_planning(
client = as_account(lead_account)
assert client.get(_url(edition, f"/{other.id}/planning")).status_code == 200
def test_export_volunteers_as_excel(client, edition, session):
position = make_position(session, edition, name="Bar central")
artist = make_artist(session, edition, name="Chilla")
volunteer = make_volunteer(
session, edition, first_name="Alex", last_name="Martin", email="alex@ex.fr"
)
client.patch(
_url(edition, f"/{volunteer.id}"),
json={
"wished_position_ids": [str(position.id)],
"artist_ids": [str(artist.id)],
},
)
resp = client.get(_url(edition, "/export"))
assert resp.status_code == 200
assert resp.headers["content-type"] == (
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
assert f"benevoles-{edition.year}.xlsx" in resp.headers["content-disposition"]
workbook = load_workbook(io.BytesIO(resp.content))
sheet = workbook.active
rows = list(sheet.iter_rows(values_only=True))
assert rows[0][0] == "Prénom"
row = rows[1]
assert row[0] == "Alex"
assert row[1] == "Martin"
assert row[2] == "alex@ex.fr"
assert row[5] == "Bar central"
assert row[6] == "Chilla"
def test_export_volunteers_is_coordinator_only(edition, make_account, as_account):
lead = make_account(Role.LEAD, email="lead@e.fr")
resp = as_account(lead).get(_url(edition, "/export"))
assert resp.status_code == 403
+2 -1
View File
@@ -4,6 +4,7 @@ from sqlmodel import select
from app.auth.tokens import hash_token
from app.models import MagicLinkToken, Role, local_now
from tests.mail import plain_text
def _url(edition, suffix):
@@ -31,7 +32,7 @@ def test_full_flow_magic_link_then_jwt_then_me(
assert resp.status_code == 202
assert len(outbox) == 1
assert outbox[0]["To"] == "ref@example.fr"
raw_token = outbox[0].get_content().split("?token=")[1].split()[0].strip()
raw_token = plain_text(outbox[0]).split("?token=")[1].split()[0].strip()
verified = raw_client.post(
_url(edition, "verify"), json={"token": raw_token}
+310 -24
View File
@@ -4,11 +4,13 @@ soumission publique (aucune authentification)."""
import base64
import datetime as dt
import io
import uuid
from app.auth.rate_limit import form_submission_limiter
from app.models import Role
from app.models import PendingFormSubmission, Role, local_now
from app.sumup import SumUpCheckout
from tests.factories import make_artist, make_position
from tests.mail import plain_text
_MERCHANT = "MXXXXX"
_API_KEY = "sup_sk_secret"
@@ -27,6 +29,24 @@ def _configure_sumup(client, edition, amount=12.5, label="Cotisation classique")
)
return resp.json()["id"]
def _pay(raw_client, edition, monkeypatch, checkout_id="co_1", status_after="PAID"):
"""Monte le checkout ensuite renvoyé par `get_checkout` sur `status_after`
(`PAID`/`FAILED`/`EXPIRED`) — les tests posent d'abord `create_checkout`
et soumettent eux-mêmes, cette aide ne fait que la partie webhook."""
monkeypatch.setattr(
"app.api.forms.get_checkout",
lambda **kw: SumUpCheckout(id=checkout_id, status=status_after, hosted_checkout_url=None),
)
return raw_client.post(
_form_url(edition, "/public/sumup-webhook"),
json={"event_type": "CHECKOUT_STATUS_CHANGED", "id": checkout_id},
)
def _pending_status(raw_client, edition, token):
return raw_client.get(_form_url(edition, f"/public/pending/{token}")).json()
# PNG 1×1 valide (transparent), le plus petit possible.
_PNG = base64.b64decode(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY"
@@ -202,7 +222,9 @@ def test_deleting_a_payment_option_keeps_the_volunteers_snapshot(
first_name="Alex", last_name="Martin", hcaptcha_token="x",
payment_option_id=option_id,
)
volunteer_id = resp.json()["volunteer_id"]
token = resp.json()["pending_token"]
_pay(raw_client, edition, monkeypatch, checkout_id="co_1")
volunteer_id = _pending_status(raw_client, edition, token)["volunteer_id"]
client.delete(_form_url(edition, f"/payment-options/{option_id}"))
@@ -294,11 +316,12 @@ def test_submitting_creates_a_volunteer(raw_client, client, edition, session):
_form_url(edition, "/fields"),
json={"fields": ["EMAIL", "PHONE", "AVAILABLE", "WILLING_TO_PAY_FEE"]},
)
# Pas de payment_option_id : « je paierai sur place » — pas de paiement
# en ligne, le bénévole est créé tout de suite.
resp = _submit(
raw_client, edition,
first_name="alex", last_name="martin", hcaptcha_token="x",
email="alex@example.org", phone="0600000000",
available="true", willing_to_pay_fee="true",
email="alex@example.org", phone="0600000000", available="true",
)
assert resp.status_code == 201
volunteer_id = resp.json()["volunteer_id"]
@@ -309,12 +332,32 @@ def test_submitting_creates_a_volunteer(raw_client, client, edition, session):
assert fiche["email"] == "alex@example.org"
assert fiche["self_declared_available"] is True
assert fiche["willing_to_pay_fee"] is True
assert fiche["fee_payment_status"] is None
# Le bénévole créé apparaît dans la liste de la coordination.
volunteers = client.get(f"/editions/{edition.id}/volunteers").json()
assert any(v["id"] == volunteer_id for v in volunteers)
def test_submitting_sends_a_registration_confirmation_email(raw_client, client, edition, outbox):
client.put(_form_url(edition, "/fields"), json={"fields": ["EMAIL"]})
resp = _submit(
raw_client, edition,
first_name="Alex", last_name="Martin", hcaptcha_token="x",
email="alex@example.org",
)
assert resp.status_code == 201
assert len(outbox) == 1
assert outbox[0]["To"] == "alex@example.org"
assert "Alex" in plain_text(outbox[0])
def test_submitting_without_an_email_sends_no_confirmation(raw_client, edition, outbox):
resp = _submit(raw_client, edition, first_name="Alex", last_name="Martin", hcaptcha_token="x")
assert resp.status_code == 201
assert outbox == []
def test_submitting_ignores_fields_not_enabled(raw_client, client, edition):
# Aucun champ activé : email/téléphone envoyés quand même, ignorés.
resp = _submit(
@@ -329,6 +372,35 @@ def test_submitting_ignores_fields_not_enabled(raw_client, client, edition):
assert fiche["phone"] is None
def test_submitting_without_an_email_is_rejected_when_email_enabled(raw_client, client, edition):
client.put(_form_url(edition, "/fields"), json={"fields": ["EMAIL"]})
resp = _submit(raw_client, edition, first_name="Alex", last_name="Martin", hcaptcha_token="x")
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "import.row.missing_required_field"
def test_submitting_without_a_phone_is_rejected_when_phone_enabled(raw_client, client, edition):
client.put(_form_url(edition, "/fields"), json={"fields": ["PHONE"]})
resp = _submit(raw_client, edition, first_name="Alex", last_name="Martin", hcaptcha_token="x")
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "import.row.missing_required_field"
def test_submitting_without_checking_available_is_rejected_when_enabled(
raw_client, client, edition
):
client.put(_form_url(edition, "/fields"), json={"fields": ["AVAILABLE"]})
resp = _submit(raw_client, edition, first_name="Alex", last_name="Martin", hcaptcha_token="x")
assert resp.status_code == 422
assert resp.json()["detail"]["code"] == "import.row.missing_required_field"
resp = _submit(
raw_client, edition,
first_name="Alex", last_name="Martin", hcaptcha_token="x", available="true",
)
assert resp.status_code == 201
def test_submitting_resolves_position_and_artist_wishes(
raw_client, client, edition, session
):
@@ -482,10 +554,11 @@ def test_submitting_with_a_rejected_captcha(raw_client, edition, monkeypatch):
assert resp.json()["detail"]["code"] == "form.captcha_failed"
def test_submitting_creates_a_sumup_checkout_when_a_payment_option_is_chosen(
def test_choosing_an_online_option_does_not_create_the_volunteer_yet(
raw_client, client, edition, monkeypatch
):
option_id = _configure_sumup(client, edition)
before = len(client.get(f"/editions/{edition.id}/volunteers").json())
monkeypatch.setattr(
"app.api.forms.create_checkout",
lambda **kw: SumUpCheckout(
@@ -499,16 +572,86 @@ def test_submitting_creates_a_sumup_checkout_when_a_payment_option_is_chosen(
)
assert resp.status_code == 201
body = resp.json()
assert body["volunteer_id"] is None
assert body["pending_token"] is not None
assert body["sumup_checkout_url"] == "https://checkout.sumup.com/pay/co_123"
fiche = client.get(f"/editions/{edition.id}/volunteers/{body['volunteer_id']}").json()
assert fiche["fee_payment_status"] == "PENDING"
# Toujours rien en base tant que le paiement n'est pas confirmé.
after = len(client.get(f"/editions/{edition.id}/volunteers").json())
assert after == before
status = _pending_status(raw_client, edition, body["pending_token"])
assert status["status"] == "PENDING"
assert status["label"] == "Cotisation classique"
assert status["amount"] == 12.5
def test_paying_promotes_the_pending_submission_to_a_volunteer(
raw_client, client, edition, monkeypatch
):
option_id = _configure_sumup(client, edition)
monkeypatch.setattr(
"app.api.forms.create_checkout",
lambda **kw: SumUpCheckout(id="co_123", status="PENDING", hosted_checkout_url="https://x"),
)
resp = _submit(
raw_client, edition,
first_name="Alex", last_name="Martin", hcaptcha_token="x",
payment_option_id=option_id,
)
token = resp.json()["pending_token"]
webhook_resp = _pay(raw_client, edition, monkeypatch, checkout_id="co_123")
assert webhook_resp.status_code == 200
status = _pending_status(raw_client, edition, token)
assert status["status"] == "PAID"
volunteer_id = status["volunteer_id"]
assert volunteer_id is not None
fiche = client.get(f"/editions/{edition.id}/volunteers/{volunteer_id}").json()
assert fiche["first_name"] == "Alex"
assert fiche["fee_payment_status"] == "PAID"
assert fiche["fee_payment_label"] == "Cotisation classique"
assert fiche["fee_amount"] == 12.5
assert fiche["fee_paid_at"] is not None
assert fiche["willing_to_pay_fee"] is True
def test_submitting_without_choosing_an_option_does_not_create_a_checkout(
def test_payment_confirmation_sends_an_email_on_promotion(
raw_client, client, edition, monkeypatch, outbox
):
client.put(_form_url(edition, "/fields"), json={"fields": ["WILLING_TO_PAY_FEE", "EMAIL"]})
client.patch(
_form_url(edition), json={"sumup_merchant_code": _MERCHANT, "sumup_api_key": _API_KEY}
)
option_id = client.post(
_form_url(edition, "/payment-options"),
json={"label": "Cotisation classique", "amount": 12.5},
).json()["id"]
monkeypatch.setattr(
"app.api.forms.create_checkout",
lambda **kw: SumUpCheckout(id="co_1", status="PENDING", hosted_checkout_url="https://x"),
)
_submit(
raw_client, edition,
first_name="Alex", last_name="Martin", hcaptcha_token="x",
email="alex@example.org",
payment_option_id=option_id,
)
assert outbox == [] # pas encore payé : pas d'email à ce stade
_pay(raw_client, edition, monkeypatch, checkout_id="co_1")
assert len(outbox) == 1
assert outbox[0]["To"] == "alex@example.org"
body = plain_text(outbox[0])
assert "Alex" in body
assert "Cotisation classique" in body
assert "12.5" in body
def test_choosing_to_pay_on_site_creates_the_volunteer_immediately(
raw_client, client, edition, monkeypatch
):
_configure_sumup(client, edition)
@@ -518,9 +661,16 @@ def test_submitting_without_choosing_an_option_does_not_create_a_checkout(
raw_client, edition, first_name="Alex", last_name="Martin", hcaptcha_token="x"
)
assert resp.status_code == 201
assert resp.json()["sumup_checkout_url"] is None
body = resp.json()
assert body["volunteer_id"] is not None
assert body["pending_token"] is None
assert body["sumup_checkout_url"] is None
assert called == []
fiche = client.get(f"/editions/{edition.id}/volunteers/{body['volunteer_id']}").json()
assert fiche["willing_to_pay_fee"] is True
assert fiche["fee_payment_status"] is None
def test_submitting_with_an_unknown_payment_option_is_rejected(raw_client, client, edition):
_configure_sumup(client, edition)
@@ -533,7 +683,7 @@ def test_submitting_with_an_unknown_payment_option_is_rejected(raw_client, clien
assert resp.json()["detail"]["code"] == "form.unknown_payment_option"
def test_a_failed_checkout_call_does_not_block_registration(
def test_a_failed_checkout_call_can_be_retried_without_resubmitting(
raw_client, client, edition, monkeypatch
):
option_id = _configure_sumup(client, edition)
@@ -546,36 +696,151 @@ def test_a_failed_checkout_call_does_not_block_registration(
assert resp.status_code == 201
body = resp.json()
assert body["sumup_checkout_url"] is None
fiche = client.get(f"/editions/{edition.id}/volunteers/{body['volunteer_id']}").json()
assert fiche["fee_payment_status"] is None
token = body["pending_token"]
assert _pending_status(raw_client, edition, token)["status"] == "PENDING"
# SumUp remarche : la relance réutilise les infos déjà saisies.
monkeypatch.setattr(
"app.api.forms.create_checkout",
lambda **kw: SumUpCheckout(id="co_retry", status="PENDING", hosted_checkout_url="https://x"),
)
retry_resp = raw_client.post(_form_url(edition, f"/public/pending/{token}/retry-payment"))
assert retry_resp.status_code == 200
assert retry_resp.json()["sumup_checkout_url"] == "https://x"
_pay(raw_client, edition, monkeypatch, checkout_id="co_retry")
assert _pending_status(raw_client, edition, token)["status"] == "PAID"
def test_sumup_webhook_updates_the_payment_status(raw_client, client, edition, monkeypatch):
def test_retrying_after_an_expired_checkout(raw_client, client, edition, monkeypatch):
option_id = _configure_sumup(client, edition)
monkeypatch.setattr(
"app.api.forms.create_checkout",
lambda **kw: SumUpCheckout(id="co_123", status="PENDING", hosted_checkout_url="https://x"),
lambda **kw: SumUpCheckout(id="co_1", status="PENDING", hosted_checkout_url="https://x"),
)
resp = _submit(
raw_client, edition,
first_name="Alex", last_name="Martin", hcaptcha_token="x",
payment_option_id=option_id,
)
volunteer_id = resp.json()["volunteer_id"]
token = resp.json()["pending_token"]
_pay(raw_client, edition, monkeypatch, checkout_id="co_1", status_after="EXPIRED")
assert _pending_status(raw_client, edition, token)["status"] == "EXPIRED"
monkeypatch.setattr(
"app.api.forms.get_checkout",
lambda **kw: SumUpCheckout(id="co_123", status="PAID", hosted_checkout_url=None),
"app.api.forms.create_checkout",
lambda **kw: SumUpCheckout(id="co_2", status="PENDING", hosted_checkout_url="https://y"),
)
webhook_resp = raw_client.post(
_form_url(edition, "/public/sumup-webhook"),
json={"event_type": "CHECKOUT_STATUS_CHANGED", "id": "co_123"},
)
assert webhook_resp.status_code == 200
retry_resp = raw_client.post(_form_url(edition, f"/public/pending/{token}/retry-payment"))
assert retry_resp.status_code == 200
def test_retrying_an_already_paid_pending_submission_is_rejected(
raw_client, client, edition, monkeypatch
):
option_id = _configure_sumup(client, edition)
monkeypatch.setattr(
"app.api.forms.create_checkout",
lambda **kw: SumUpCheckout(id="co_1", status="PENDING", hosted_checkout_url="https://x"),
)
resp = _submit(
raw_client, edition,
first_name="Alex", last_name="Martin", hcaptcha_token="x",
payment_option_id=option_id,
)
token = resp.json()["pending_token"]
_pay(raw_client, edition, monkeypatch, checkout_id="co_1")
retry_resp = raw_client.post(_form_url(edition, f"/public/pending/{token}/retry-payment"))
assert retry_resp.status_code == 422
assert retry_resp.json()["detail"]["code"] == "form.pending_already_paid"
def test_an_unknown_pending_token_is_a_404(raw_client, edition):
resp = raw_client.get(
_form_url(edition, "/public/pending/00000000-0000-0000-0000-000000000000")
)
assert resp.status_code == 404
def test_choosing_a_mecenat_option_requires_the_address_step_first(
raw_client, client, edition, monkeypatch
):
client.put(_form_url(edition, "/fields"), json={"fields": ["WILLING_TO_PAY_FEE"]})
client.patch(
_form_url(edition), json={"sumup_merchant_code": _MERCHANT, "sumup_api_key": _API_KEY}
)
option_id = client.post(
_form_url(edition, "/payment-options"),
json={"label": "Mécénat", "amount": 50, "requires_mecenat_info": True},
).json()["id"]
called = []
monkeypatch.setattr("app.api.forms.create_checkout", lambda **kw: called.append(kw))
resp = _submit(
raw_client, edition,
first_name="Alex", last_name="Martin", hcaptcha_token="x",
payment_option_id=option_id,
)
assert resp.status_code == 201
body = resp.json()
assert body["requires_mecenat_info"] is True
assert body["sumup_checkout_url"] is None
assert called == [] # pas de checkout avant d'avoir l'adresse
token = body["pending_token"]
monkeypatch.setattr(
"app.api.forms.create_checkout",
lambda **kw: SumUpCheckout(id="co_don", status="PENDING", hosted_checkout_url="https://don"),
)
info_resp = raw_client.post(
_form_url(edition, f"/public/pending/{token}/mecenat-info"),
json={
"address": "1 rue du Festival",
"city": "Rennes",
"postal_code": "35000",
"country": "France",
},
)
assert info_resp.status_code == 200
assert info_resp.json()["sumup_checkout_url"] == "https://don"
_pay(raw_client, edition, monkeypatch, checkout_id="co_don")
volunteer_id = _pending_status(raw_client, edition, token)["volunteer_id"]
fiche = client.get(f"/editions/{edition.id}/volunteers/{volunteer_id}").json()
assert fiche["fee_payment_status"] == "PAID"
assert fiche["fee_paid_at"] is not None
assert fiche["mecenat_address"] == "1 rue du Festival"
assert fiche["mecenat_city"] == "Rennes"
assert fiche["mecenat_postal_code"] == "35000"
assert fiche["mecenat_country"] == "France"
def test_mecenat_info_requires_every_field(raw_client, client, edition, monkeypatch):
client.put(_form_url(edition, "/fields"), json={"fields": ["WILLING_TO_PAY_FEE"]})
client.patch(
_form_url(edition), json={"sumup_merchant_code": _MERCHANT, "sumup_api_key": _API_KEY}
)
option_id = client.post(
_form_url(edition, "/payment-options"),
json={"label": "Mécénat", "amount": 50, "requires_mecenat_info": True},
).json()["id"]
resp = _submit(
raw_client, edition,
first_name="Alex", last_name="Martin", hcaptcha_token="x",
payment_option_id=option_id,
)
token = resp.json()["pending_token"]
info_resp = raw_client.post(
_form_url(edition, f"/public/pending/{token}/mecenat-info"),
json={
"address": "1 rue du Festival",
"city": "Rennes",
"postal_code": "",
"country": "France",
},
)
assert info_resp.status_code == 422
def test_sumup_webhook_ignores_an_unknown_checkout(raw_client, client, edition, monkeypatch):
@@ -591,6 +856,27 @@ def test_sumup_webhook_ignores_an_unknown_checkout(raw_client, client, edition,
assert resp.status_code == 200
def test_stale_pending_submissions_are_pruned(raw_client, client, edition, session, monkeypatch):
option_id = _configure_sumup(client, edition)
monkeypatch.setattr("app.api.forms.create_checkout", lambda **kw: None)
resp = _submit(
raw_client, edition,
first_name="Alex", last_name="Martin", hcaptcha_token="x",
payment_option_id=option_id,
)
token = resp.json()["pending_token"]
pending = session.get(PendingFormSubmission, uuid.UUID(token))
pending.created_at = local_now() - dt.timedelta(hours=49)
session.add(pending)
session.commit()
# N'importe quelle nouvelle soumission déclenche le nettoyage paresseux.
_submit(raw_client, edition, first_name="Sam", last_name="Dupuis", hcaptcha_token="x")
assert raw_client.get(_form_url(edition, f"/public/pending/{token}")).status_code == 404
def test_submitting_is_rate_limited_per_edition(raw_client, edition):
for _ in range(form_submission_limiter.max_attempts):
form_submission_limiter.hit(str(edition.id))
+1
View File
@@ -32,6 +32,7 @@ EXPECTED_TABLES = {
"form_template",
"form_field",
"form_payment_option",
"pending_form_submission",
}