"""Tests for ReminderDispatcher (U5).""" from __future__ import annotations import sys from unittest.mock import AsyncMock, MagicMock, patch from agentkit.calendar.models import CalendarEvent, _now_iso from agentkit.calendar.reminders import ReminderDispatcher, SmtpConfig def _make_event() -> CalendarEvent: now = _now_iso() return CalendarEvent( id="evt-1", user_id="user-1", title="Test Meeting", start_time="2026-07-01T10:00:00+00:00", end_time="2026-07-01T11:00:00+00:00", last_modified=now, created_at=now, ) # --------------------------------------------------------------------------- # Client channel # --------------------------------------------------------------------------- async def test_client_channel_sends_ws_message() -> None: """Mock WS sender callback, verify called with correct payload.""" ws_sender = AsyncMock() dispatcher = ReminderDispatcher(ws_sender=ws_sender) event = _make_event() result = await dispatcher.dispatch("client", event, "user-1") assert result is True ws_sender.assert_called_once() call_args = ws_sender.call_args assert call_args.args[0] == "user-1" assert call_args.args[1]["type"] == "calendar_reminder" assert call_args.args[1]["data"]["title"] == "Test Meeting" async def test_client_channel_returns_false_without_sender() -> None: """No ws_sender configured → returns False.""" dispatcher = ReminderDispatcher() result = await dispatcher.dispatch("client", _make_event(), "user-1") assert result is False # --------------------------------------------------------------------------- # Email channel # --------------------------------------------------------------------------- async def test_email_channel_sends_smtp() -> None: """Mock aiosmtplib via sys.modules injection, verify send called.""" mock_aiosmtplib = MagicMock() mock_aiosmtplib.send = AsyncMock() with patch.dict(sys.modules, {"aiosmtplib": mock_aiosmtplib}): dispatcher = ReminderDispatcher( smtp_config=SmtpConfig(host="smtp.example.com", port=587), get_user_email=AsyncMock(return_value="user@example.com"), ) event = _make_event() result = await dispatcher.dispatch("email", event, "user-1") assert result is True mock_aiosmtplib.send.assert_called_once() call_kwargs = mock_aiosmtplib.send.call_args.kwargs assert call_kwargs["hostname"] == "smtp.example.com" assert call_kwargs["port"] == 587 # Message body contains event title and recipient message_body = mock_aiosmtplib.send.call_args.args[0] assert "user@example.com" in message_body assert "Test Meeting" in message_body async def test_email_channel_returns_false_without_config() -> None: """No smtp_config → returns False.""" dispatcher = ReminderDispatcher() result = await dispatcher.dispatch("email", _make_event(), "user-1") assert result is False async def test_email_channel_returns_false_when_user_has_no_email() -> None: """get_user_email returns None → returns False.""" dispatcher = ReminderDispatcher( smtp_config=SmtpConfig(), get_user_email=AsyncMock(return_value=None), ) result = await dispatcher.dispatch("email", _make_event(), "user-1") assert result is False # --------------------------------------------------------------------------- # Webhook channel # --------------------------------------------------------------------------- async def test_webhook_channel_posts_to_url() -> None: """Mock httpx.AsyncClient, verify POST called with event payload.""" dispatcher = ReminderDispatcher(webhook_url="https://example.com/hook") mock_response = MagicMock() mock_response.status_code = 200 mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__.return_value = mock_client mock_client.__aexit__.return_value = None with patch("httpx.AsyncClient", return_value=mock_client): event = _make_event() result = await dispatcher.dispatch("webhook", event, "user-1") assert result is True mock_client.post.assert_called_once() call_kwargs = mock_client.post.call_args.kwargs assert call_kwargs["json"]["event"]["title"] == "Test Meeting" assert call_kwargs["json"]["user_id"] == "user-1" async def test_webhook_channel_returns_false_on_4xx() -> None: """Webhook returns 500 → returns False.""" dispatcher = ReminderDispatcher(webhook_url="https://example.com/hook") mock_response = MagicMock() mock_response.status_code = 500 mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__.return_value = mock_client mock_client.__aexit__.return_value = None with patch("httpx.AsyncClient", return_value=mock_client): result = await dispatcher.dispatch("webhook", _make_event(), "user-1") assert result is False async def test_webhook_channel_returns_false_without_url() -> None: """No webhook_url configured → returns False.""" dispatcher = ReminderDispatcher() result = await dispatcher.dispatch("webhook", _make_event(), "user-1") assert result is False # --------------------------------------------------------------------------- # Unknown channel # --------------------------------------------------------------------------- async def test_unknown_channel_returns_false() -> None: """Unknown channel name → returns False, no crash.""" dispatcher = ReminderDispatcher() result = await dispatcher.dispatch("sms", _make_event(), "user-1") assert result is False