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
105 lines
3.2 KiB
Python
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"]
|