Files
Meister-Eder/tests/test_notifier.py
T
Claude 7f55cdd204 Add pytest test suite (92 tests, all passing)
Covers every module in src/ with unit tests:

- tests/conftest.py        shared fixtures (complete_registration, fresh_state, …)
- tests/test_models.py     RegistrationData.is_complete(), to_dict/from_dict round-trips
- tests/test_storage.py    normalize_email, _diff_registrations, ConversationStore CRUD,
                           registration versioning
- tests/test_llm.py        litellm wrapper — message construction, model passthrough,
                           error propagation
- tests/test_agent.py      EmailAgent — new/existing conversations, registration
                           completion, admin notification, fallback on LLM error,
                           JSON parsing, _apply_updates
- tests/test_notifier.py   AdminNotifier routing, fee calculation, SMTP dispatch
- tests/test_knowledge_base.py  KnowledgeBase loading and reload

All external I/O (litellm, SMTP, filesystem) is mocked. Tests run fast (~6s)
with no network access required.

https://claude.ai/code/session_01HaUFs7SaLD5SoiuGCY27Tw
2026-02-21 08:07:31 +00:00

156 lines
5.3 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",
)
# ---------------------------------------------------------------------------
# _recipients_for
# ---------------------------------------------------------------------------
class TestRecipientsFor:
def test_indoor_only_recipients(self, notifier):
recipients = notifier._recipients_for(["indoor"])
assert any("andrea" in r.lower() or "sigrist" in r.lower() for r in recipients)
def test_outdoor_only_recipients(self, notifier):
recipients = notifier._recipients_for(["outdoor"])
assert any("baba.laeubli" in r.lower() for r in recipients)
def test_both_includes_both_leaders(self, notifier):
recipients = notifier._recipients_for(["indoor", "outdoor"])
joined = " ".join(recipients).lower()
assert "andrea.sigrist" in joined
assert "baba.laeubli" in joined
# ---------------------------------------------------------------------------
# _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