Previously routing was hardcoded (Andrea for indoor, Barbara for outdoor). Then it was replaced with a flat ADMIN_EMAILS list which lost the routing. This commit restores routing via three separate env vars: ADMIN_EMAIL_INDOOR — indoor leader, To when indoor days are booked ADMIN_EMAIL_OUTDOOR — outdoor leader, To when outdoor days are booked ADMIN_EMAIL_CC — always Cc'd (comma-separated for multiple) For testing, set all three to your own address so no real leader gets mail. Changes: - Config: replaced admin_emails with admin_email_indoor/outdoor/cc fields - AdminNotifier: replaced admin_emails param with indoor_email/outdoor_email/ cc_emails; _recipients_for() restored as an instance method using these - main.py: wires the three new config fields into AdminNotifier - .env.example: documents the three new variables with production defaults - Tests: fixture updated to use new params https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
138 lines
4.6 KiB
Python
138 lines
4.6 KiB
Python
"""Tests for AdminNotifier helper methods."""
|
|
|
|
import pytest
|
|
|
|
from src.notifications.notifier import AdminNotifier
|
|
from src.models.registration import RegistrationData, Booking, BookingDay
|
|
|
|
|
|
@pytest.fixture
|
|
def notifier():
|
|
return AdminNotifier(
|
|
smtp_host="smtp.example.com",
|
|
smtp_port=587,
|
|
username="agent@example.com",
|
|
password="secret",
|
|
use_tls=True,
|
|
from_email="agent@example.com",
|
|
indoor_email="andrea@example.com",
|
|
outdoor_email="barbara@example.com",
|
|
cc_emails=["markus@example.com"],
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _format_types
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFormatTypes:
|
|
def test_indoor_label(self, notifier):
|
|
assert "Innen" in notifier._format_types(["indoor"]) or "indoor" in notifier._format_types(["indoor"]).lower()
|
|
|
|
def test_outdoor_label(self, notifier):
|
|
assert "Wald" in notifier._format_types(["outdoor"]) or "outdoor" in notifier._format_types(["outdoor"]).lower()
|
|
|
|
def test_both_labels(self, notifier):
|
|
result = notifier._format_types(["indoor", "outdoor"])
|
|
assert len(result) > 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _calculate_age
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCalculateAge:
|
|
def test_returns_age_string(self, notifier):
|
|
result = notifier._calculate_age("2022-01-01")
|
|
assert isinstance(result, str)
|
|
assert len(result) > 0
|
|
|
|
def test_invalid_dob_returns_original_string(self, notifier):
|
|
result = notifier._calculate_age("not-a-date")
|
|
assert result == "not-a-date"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _calculate_monthly_fee
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCalculateMonthlyFee:
|
|
def test_indoor_one_day(self, notifier, complete_registration):
|
|
complete_registration.booking = Booking(
|
|
playgroup_types=["indoor"],
|
|
selected_days=[BookingDay(day="monday", type="indoor")],
|
|
)
|
|
fee = notifier._calculate_monthly_fee(complete_registration)
|
|
assert "130" in fee
|
|
|
|
def test_indoor_two_days(self, notifier, complete_registration):
|
|
complete_registration.booking = Booking(
|
|
playgroup_types=["indoor"],
|
|
selected_days=[
|
|
BookingDay(day="monday", type="indoor"),
|
|
BookingDay(day="wednesday", type="indoor"),
|
|
],
|
|
)
|
|
fee = notifier._calculate_monthly_fee(complete_registration)
|
|
assert "260" in fee
|
|
|
|
def test_indoor_three_days(self, notifier, complete_registration):
|
|
complete_registration.booking = Booking(
|
|
playgroup_types=["indoor"],
|
|
selected_days=[
|
|
BookingDay(day="monday", type="indoor"),
|
|
BookingDay(day="wednesday", type="indoor"),
|
|
BookingDay(day="thursday", type="indoor"),
|
|
],
|
|
)
|
|
fee = notifier._calculate_monthly_fee(complete_registration)
|
|
assert "390" in fee
|
|
|
|
def test_outdoor_one_day(self, notifier, complete_registration):
|
|
complete_registration.booking = Booking(
|
|
playgroup_types=["outdoor"],
|
|
selected_days=[BookingDay(day="monday", type="outdoor")],
|
|
)
|
|
fee = notifier._calculate_monthly_fee(complete_registration)
|
|
assert "250" in fee
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _send — SMTP interaction
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSend:
|
|
def test_send_calls_smtp(self, notifier, mocker):
|
|
# _send uses smtplib.SMTP directly (not as context manager)
|
|
mock_smtp_cls = mocker.patch("smtplib.SMTP")
|
|
mock_server = mock_smtp_cls.return_value
|
|
|
|
notifier._send(
|
|
to=["admin@example.com"],
|
|
cc=["cc@example.com"],
|
|
subject="Test",
|
|
body="Hello",
|
|
)
|
|
|
|
mock_server.sendmail.assert_called_once()
|
|
|
|
def test_send_includes_all_recipients(self, notifier, mocker):
|
|
mock_smtp_cls = mocker.patch("smtplib.SMTP")
|
|
mock_server = mock_smtp_cls.return_value
|
|
|
|
notifier._send(
|
|
to=["a@example.com"],
|
|
cc=["b@example.com"],
|
|
subject="Test",
|
|
body="Hello",
|
|
)
|
|
|
|
call_args = mock_server.sendmail.call_args
|
|
recipients = call_args[0][1] # positional arg: to_addrs
|
|
assert "a@example.com" in recipients
|
|
assert "b@example.com" in recipients
|