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
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user