Booking confirmation email + customer self-service cancel/reschedule (#18)
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>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""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 == []
|
||||
Reference in New Issue
Block a user