Files
032-birthday-wisher/tests/unit/application/use_cases/test_send_birthday_wishes.py
Tirsvad 0126093601 Implement the birthday wisher core (MIL-002)
Layered package (domain, application, infrastructure, presentation), CLI with
--dry-run and --today, SMTP with STARTTLS behind a port, 56 offline tests and
the Python code review RC-006.

Task: MIL-002#1
Task: MIL-002#2
Task: MIL-002#3
Task: MIL-002#4
Task: MIL-002#5
Task: MIL-002#6
Refs #7
Refs #8
Refs #9
Refs #10
Refs #11
Refs #12
2026-10-17 15:37:16 +08:00

105 lines
3.2 KiB
Python

"""Unit tests for the send-birthday-wishes use case, using fakes of the ports."""
from datetime import date
from birthday_wisher import constants
from birthday_wisher.application.use_cases.send_birthday_wishes import (
birthdays_by_day,
build_letter,
find_birthday_people,
send_birthday_wishes,
)
from birthday_wisher.domain.errors import MailError
from birthday_wisher.domain.person import Person
TODAY = date(2026, 10, 17)
ALICE = Person("Alice", "alice@example.com", 1990, 3, 14)
BOB = Person("Bob", "bob@example.com", 1985, 10, 17)
CAROL = Person("Carol", "carol@example.com", 2001, 10, 17)
class FakeRepository:
"""In-memory BirthdayRepository."""
def __init__(self, people: list[Person]) -> None:
self._people = people
def load_all(self) -> list[Person]:
return list(self._people)
class FakeLetters:
"""In-memory LetterSource."""
def read_letter(self, number: int) -> str:
return f"Letter {number} for [NAME]"
class FakeMailer:
"""Records mails; fails for the addresses in `fail_for`."""
def __init__(self, fail_for: set[str] | None = None) -> None:
self.sent: list[tuple[str, str, str]] = []
self._fail_for = fail_for or set()
def send(self, recipient: str, subject: str, body: str) -> None:
if recipient in self._fail_for:
raise MailError("boom")
self.sent.append((recipient, subject, body))
def test_find_birthday_people_selects_only_today() -> None:
assert find_birthday_people([ALICE, BOB], TODAY) == [BOB]
def test_find_birthday_people_returns_both_sharing_a_day() -> None:
assert find_birthday_people([BOB, ALICE, CAROL], TODAY) == [BOB, CAROL]
def test_find_birthday_people_empty_when_nobody() -> None:
assert find_birthday_people([ALICE], TODAY) == []
def test_birthdays_by_day_groups_people_sharing_a_day() -> None:
result = birthdays_by_day([ALICE, BOB, CAROL])
assert result == {(3, 14): [ALICE], (10, 17): [BOB, CAROL]}
def test_build_letter_replaces_name() -> None:
assert build_letter("Dear [NAME],", "Bob") == "Dear Bob,"
def test_build_letter_replaces_every_placeholder() -> None:
assert build_letter("[NAME] and [NAME]", "Bo") == "Bo and Bo"
def test_send_wishes_sends_one_letter_to_todays_person_only() -> None:
mailer = FakeMailer()
report = send_birthday_wishes(
FakeRepository([ALICE, BOB]), FakeLetters(), mailer, TODAY, lambda: 2
)
assert mailer.sent == [
("bob@example.com", constants.EMAIL_SUBJECT, "Letter 2 for Bob")
]
assert report.sent == ["Bob"]
assert report.failed == []
def test_send_wishes_with_no_birthday_sends_nothing() -> None:
mailer = FakeMailer()
report = send_birthday_wishes(
FakeRepository([ALICE]), FakeLetters(), mailer, TODAY, lambda: 1
)
assert mailer.sent == []
assert report.sent == [] and report.failed == []
def test_send_wishes_one_failure_does_not_stop_the_others() -> None:
mailer = FakeMailer(fail_for={"bob@example.com"})
report = send_birthday_wishes(
FakeRepository([BOB, CAROL]), FakeLetters(), mailer, TODAY, lambda: 1
)
assert report.failed == ["Bob"]
assert report.sent == ["Carol"]
assert [m[0] for m in mailer.sent] == ["carol@example.com"]