L'email de test reprend le modèle en cours d'édition, balises remplies avec le planning d'un bénévole au choix (premier avec un créneau par défaut), option lien de connexion sans vrai jeton, sujet « [Test] … » — même fonction de rendu que l'envoi réel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
5.2 KiB
Python
145 lines
5.2 KiB
Python
"""Email de test depuis l'écran « Messages » (`POST .../notifications/test`) :
|
|
envoi synchrone, résultat réel du serveur SMTP renvoyé à la coordination."""
|
|
|
|
import smtplib
|
|
|
|
import pytest
|
|
|
|
from app.config import get_settings
|
|
from app.models import Role
|
|
|
|
|
|
def _url(edition):
|
|
return f"/editions/{edition.id}/notifications/test"
|
|
|
|
|
|
class _FakeSmtp:
|
|
def __init__(self, error=None):
|
|
self.sent = []
|
|
self.error = error
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *exc):
|
|
return False
|
|
|
|
def send_message(self, message):
|
|
if self.error:
|
|
raise self.error
|
|
self.sent.append(message)
|
|
|
|
|
|
@pytest.fixture
|
|
def smtp_enabled(monkeypatch):
|
|
monkeypatch.setenv("EMAIL_ENABLED", "true")
|
|
get_settings.cache_clear()
|
|
|
|
|
|
def test_disabled_email_is_reported(client, edition):
|
|
resp = client.post(_url(edition), json={"email": "moi@exemple.fr"})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["status"] == "disabled"
|
|
|
|
|
|
def test_sends_the_test_email(client, edition, smtp_enabled, monkeypatch):
|
|
fake = _FakeSmtp()
|
|
monkeypatch.setattr("app.email.sender.open_smtp", lambda settings: fake)
|
|
resp = client.post(_url(edition), json={"email": " moi@exemple.fr "})
|
|
assert resp.json()["status"] == "sent"
|
|
(message,) = fake.sent
|
|
assert message["To"] == "moi@exemple.fr"
|
|
|
|
|
|
def test_connection_failure_is_reported_with_the_server_message(
|
|
client, edition, smtp_enabled, monkeypatch
|
|
):
|
|
def refuse(settings):
|
|
raise smtplib.SMTPAuthenticationError(535, b"authentication failed")
|
|
|
|
monkeypatch.setattr("app.email.sender.open_smtp", refuse)
|
|
body = client.post(_url(edition), json={"email": "moi@exemple.fr"}).json()
|
|
assert body["status"] == "connection_failed"
|
|
assert "535" in body["detail"]
|
|
|
|
|
|
def test_rejected_message_never_echoes_the_address(client, edition, smtp_enabled, monkeypatch):
|
|
refused = smtplib.SMTPRecipientsRefused({"moi@exemple.fr": (550, b"no such user")})
|
|
monkeypatch.setattr("app.email.sender.open_smtp", lambda settings: _FakeSmtp(refused))
|
|
body = client.post(_url(edition), json={"email": "moi@exemple.fr"}).json()
|
|
assert body["status"] == "rejected"
|
|
assert "550" in body["detail"]
|
|
assert "moi@exemple.fr" not in body["detail"]
|
|
|
|
|
|
@pytest.mark.parametrize("email", ["", "pas-un-email", "@exemple.fr", "moi@", "a b@c.fr"])
|
|
def test_invalid_address_is_rejected(client, edition, email):
|
|
resp = client.post(_url(edition), json={"email": email})
|
|
assert resp.status_code == 422
|
|
|
|
|
|
def test_is_coordinator_only(edition, make_account, as_account):
|
|
lead = as_account(make_account(Role.LEAD))
|
|
assert lead.post(_url(edition), json={"email": "moi@exemple.fr"}).status_code == 403
|
|
|
|
|
|
def _send(client, edition, monkeypatch, **payload):
|
|
captured = []
|
|
monkeypatch.setattr(
|
|
"app.api.notifications.deliver_test",
|
|
lambda message: captured.append(message) or ("sent", None),
|
|
)
|
|
body = client.post(_url(edition), json={"email": "moi@exemple.fr", **payload}).json()
|
|
return body, captured[0]
|
|
|
|
|
|
def test_test_email_is_the_compiled_planning_email(client, edition, session, monkeypatch):
|
|
from app.models import Assignment
|
|
from tests.factories import dt_at, make_position, make_shift, make_volunteer
|
|
from tests.mail import plain_text
|
|
|
|
make_volunteer(session, edition, first_name="Zoé", last_name="Zébu") # sans créneau
|
|
alex = make_volunteer(session, edition, first_name="Alex", last_name="Martin")
|
|
shift = make_shift(session, edition, make_position(session, edition, name="Bar"),
|
|
start=dt_at(10, 18), end=dt_at(10, 22))
|
|
session.add(Assignment(edition_id=edition.id, volunteer_id=alex.id, shift_id=shift.id))
|
|
session.commit()
|
|
|
|
body, message = _send(
|
|
client, edition, monkeypatch,
|
|
subject="Planning de {prenom}",
|
|
body="Salut {prenom} {nom} : {nb_creneaux} créneau(x), {nb_heures} h\n{creneaux}",
|
|
)
|
|
# Premier bénévole (par nom) qui a un créneau, pas Zoé.
|
|
assert body["example_volunteer_name"] == "Alex Martin"
|
|
assert message["Subject"] == "[Test] Planning de Alex"
|
|
text = plain_text(message)
|
|
assert "Salut Alex Martin : 1 créneau(x), 4.0 h" in text
|
|
assert "Bar" in text
|
|
assert "{" not in text
|
|
|
|
|
|
def test_login_link_in_test_email_carries_no_token(client, edition, session, monkeypatch):
|
|
from sqlmodel import select
|
|
|
|
from app.models import MagicLinkToken
|
|
from tests.factories import make_volunteer
|
|
from tests.mail import plain_text
|
|
|
|
make_volunteer(session, edition)
|
|
_, message = _send(client, edition, monkeypatch, body="Connexion : {lien_connexion}")
|
|
assert "token=" not in plain_text(message)
|
|
assert session.exec(select(MagicLinkToken)).all() == []
|
|
|
|
|
|
def test_chosen_volunteer_and_fictitious_example(client, edition, session, monkeypatch):
|
|
from tests.factories import make_volunteer
|
|
|
|
body, message = _send(client, edition, monkeypatch, subject="Pour {prenom}")
|
|
assert body["example_volunteer_id"] is None
|
|
assert message["Subject"] == "[Test] Pour Camille"
|
|
|
|
sam = make_volunteer(session, edition, first_name="Sam", last_name="Nuit")
|
|
body, _ = _send(client, edition, monkeypatch, volunteer_id=str(sam.id))
|
|
assert body["example_volunteer_id"] == str(sam.id)
|