161 lines
5.7 KiB
Python
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
|