fischer-agentkit/tests/unit/calendar/test_reminders.py

161 lines
5.7 KiB
Python

"""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