644c99ee30
Sends a confirmation email (best-effort, fire-and-forget SMTP via mailer.py) on booking creation, with a manage-booking link embedding the ticket-2 signed token. Adds /manage/<token>, a stateless cancel/reschedule page that reuses the existing slot-picker against booking_api's create/cancel/reschedule API, distinguishing an invalid/expired link from an already-cancelled one. Sender address uses the client's own domain when configured, falling back to a mivanchenko.de address otherwise. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
94 lines
3.5 KiB
Python
94 lines
3.5 KiB
Python
"""Unit tests for booking_mail.py (#18): sender-address resolution and the
|
|
skip-if-not-an-email guard, per the acceptance criterion that customer_contact
|
|
(free-text "E-Mail oder Telefon") may not actually be an email address.
|
|
"""
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import pytest
|
|
|
|
import booking_mail
|
|
from app import app as flask_app
|
|
|
|
|
|
@pytest.mark.parametrize("contact,expected", [
|
|
("alice@example.com", True),
|
|
("Alice@Example.COM", True),
|
|
("+49 151 2345678", False),
|
|
("0151-2345678", False),
|
|
("not-an-email", False),
|
|
("", False),
|
|
(None, False),
|
|
])
|
|
def test_looks_like_email(contact, expected):
|
|
assert booking_mail.looks_like_email(contact) is expected
|
|
|
|
|
|
def test_manage_url_embeds_token(monkeypatch):
|
|
monkeypatch.setattr(booking_mail, "PUBLIC_BASE_URL", "https://onboard.example.com")
|
|
assert booking_mail.manage_url("abc.def.ghi") == \
|
|
"https://onboard.example.com/manage/abc.def.ghi"
|
|
|
|
|
|
def test_sender_uses_client_domain_when_configured():
|
|
client = {"domain": "happynails.de"}
|
|
assert booking_mail._sender_for(client) == "noreply@happynails.de"
|
|
|
|
|
|
def test_sender_falls_back_when_client_has_no_domain(monkeypatch):
|
|
monkeypatch.setattr(booking_mail.mailer, "MAIL_FALLBACK_FROM", "noreply@mivanchenko.de")
|
|
assert booking_mail._sender_for({"domain": None}) == "noreply@mivanchenko.de"
|
|
assert booking_mail._sender_for({}) == "noreply@mivanchenko.de"
|
|
assert booking_mail._sender_for(None) == "noreply@mivanchenko.de"
|
|
|
|
|
|
@pytest.mark.parametrize("raw,expected", [
|
|
("happynails.de", "happynails.de"),
|
|
("https://happynails.de", "happynails.de"),
|
|
("https://happynails.de/", "happynails.de"),
|
|
("http://happynails.de/shop", "happynails.de"),
|
|
(" happynails.de ", "happynails.de"),
|
|
("HappyNails.de", "happynails.de"),
|
|
])
|
|
def test_sender_sanitizes_domain_entered_with_scheme_or_path(raw, expected):
|
|
assert booking_mail._sender_for({"domain": raw}) == f"noreply@{expected}"
|
|
|
|
|
|
def _booking(contact="alice@example.com"):
|
|
start = datetime.now(timezone.utc) + timedelta(days=1)
|
|
return {
|
|
"customer_name": "Alice",
|
|
"customer_contact": contact,
|
|
"service": "Haircut",
|
|
"start_time": start,
|
|
"status": "confirmed",
|
|
}
|
|
|
|
|
|
def test_send_booking_confirmation_sends_when_contact_is_email(monkeypatch):
|
|
captured = []
|
|
monkeypatch.setattr(booking_mail.mailer, "send_email",
|
|
lambda to, subject, html, from_addr=None:
|
|
captured.append((to, subject, html, from_addr)))
|
|
client = {"domain": "happynails.de", "business_name": "Happy Nails",
|
|
"timezone": "Europe/Berlin"}
|
|
with flask_app.test_request_context():
|
|
booking_mail.send_booking_confirmation(client, _booking(), "sometoken")
|
|
assert len(captured) == 1
|
|
to, subject, html, from_addr = captured[0]
|
|
assert to == "alice@example.com"
|
|
assert from_addr == "noreply@happynails.de"
|
|
assert "Happy Nails" in subject
|
|
assert "/manage/sometoken" in html
|
|
assert "Haircut" in html
|
|
|
|
|
|
def test_send_booking_confirmation_skips_when_contact_is_phone(monkeypatch):
|
|
captured = []
|
|
monkeypatch.setattr(booking_mail.mailer, "send_email",
|
|
lambda *a, **kw: captured.append((a, kw)))
|
|
client = {"business_name": "Happy Nails", "timezone": "Europe/Berlin"}
|
|
with flask_app.test_request_context():
|
|
booking_mail.send_booking_confirmation(client, _booking(contact="0151-2345678"),
|
|
"sometoken")
|
|
assert captured == []
|