test(integration): match team member alert deliveries by subject so other alerts cannot race the count

This commit is contained in:
ryan-crabbe-berri 2026-09-24 15:05:43 -07:00
parent 8b083c9523
commit 3dd49e0dbf
2 changed files with 9 additions and 15 deletions

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import socketserver
import threading
from collections import deque
from collections.abc import Generator
from contextlib import contextmanager
from dataclasses import dataclass
@ -33,13 +34,10 @@ class Delivery:
class Mailbox:
host: str
port: int
received: SimpleQueue[Delivery]
received: deque[Delivery]
def pending(self) -> int:
return self.received.qsize()
def drain(self) -> tuple[Delivery, ...]:
return tuple(self.received.get_nowait() for _ in range(self.received.qsize()))
def with_subject(self, fragment: str) -> tuple[Delivery, ...]:
return tuple(delivery for delivery in tuple(self.received) if fragment in delivery.subject)
def _address(argument: str) -> str:
@ -49,7 +47,7 @@ def _address(argument: str) -> str:
@contextmanager
def smtp_sink() -> Generator[Mailbox, None, None]:
"""Owned plaintext SMTP peer; deliveries traverse the proxy's real smtplib client."""
received: Final[SimpleQueue[Delivery]] = SimpleQueue()
received: Final[deque[Delivery]] = deque() # mutable-ok: sink thread appends each delivery
errors: Final[SimpleQueue[Exception]] = SimpleQueue()
class Handler(socketserver.StreamRequestHandler):
@ -92,7 +90,7 @@ def smtp_sink() -> Generator[Mailbox, None, None]:
if not chunk or chunk == b".\r\n":
break
body.extend(chunk[1:] if chunk.startswith(b"..") else chunk)
received.put(Delivery(sender, recipients, message_from_bytes(bytes(body))))
received.append(Delivery(sender, recipients, message_from_bytes(bytes(body))))
sender, recipients = "", ()
self._reply("250 OK queued")
elif verb == "RSET":

View file

@ -56,7 +56,7 @@ def test_team_member_budget_thresholds_email_member_and_configured_recipients(ga
eventually(
lambda: _membership_spend(user_id, team_id), lambda spend: spend == pytest.approx(CALL_COST), seconds=70
)
assert mailbox.drain() == (), "no threshold is reached before the first call is recorded"
assert tuple(mailbox.received) == (), "no threshold is reached before the first call is recorded"
second: Final = candidate.request(
"POST",
@ -65,10 +65,8 @@ def test_team_member_budget_thresholds_email_member_and_configured_recipients(ga
key=key,
)
assert second.status_code == 200, second.text
eventually(mailbox.pending, lambda count: count >= 1, seconds=30)
halfway: Final = mailbox.drain()
halfway: Final = eventually(lambda: mailbox.with_subject("50%"), lambda found: len(found) >= 1, seconds=30)
assert [delivery.recipients for delivery in halfway] == [(member_email,)], halfway
assert "50%" in halfway[0].subject, halfway[0].subject
assert f"${MEMBER_BUDGET}" in halfway[0].html, halfway[0].html
eventually(
lambda: _membership_spend(user_id, team_id),
@ -83,9 +81,7 @@ def test_team_member_budget_thresholds_email_member_and_configured_recipients(ga
key=key,
)
assert third.status_code == 422 and third.json()["error"]["type"] == "budget_exceeded", third.text
eventually(mailbox.pending, lambda count: count >= 2, seconds=30)
hundred: Final = mailbox.drain()
assert all("100%" in delivery.subject for delivery in hundred), hundred
hundred: Final = eventually(lambda: mailbox.with_subject("100%"), lambda found: len(found) >= 2, seconds=30)
assert {recipient for delivery in hundred for recipient in delivery.recipients} == {
member_email,
finance_email,