All checks were successful
CI / lint-and-test (push) Successful in 22s
- Added support for filtering calendar events by type in the ICS generation API endpoint, allowing users to specify whether to include only duty shifts or all event types (duty, unavailable, vacation). - Updated the `get_duties_for_user` function to accept an optional `event_types` parameter, enabling more flexible data retrieval based on user preferences. - Enhanced unit tests to cover the new event type filtering functionality, ensuring correct behavior and reliability of the ICS generation process.
254 lines
10 KiB
Python
254 lines
10 KiB
Python
"""Tests for duty_teller.handlers.commands (start, set_phone, calendar_link, help_cmd)."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from duty_teller.handlers.commands import (
|
|
start,
|
|
set_phone,
|
|
calendar_link,
|
|
help_cmd,
|
|
)
|
|
|
|
|
|
def _make_update(message=None, effective_user=None, effective_chat=None):
|
|
"""Build a minimal Update with message and effective_user/chat."""
|
|
update = MagicMock()
|
|
update.message = message
|
|
update.effective_user = effective_user
|
|
update.effective_chat = effective_chat
|
|
return update
|
|
|
|
|
|
def _make_user(
|
|
user_id=1,
|
|
first_name="Test",
|
|
last_name="User",
|
|
username="testuser",
|
|
):
|
|
"""Build a minimal Telegram user."""
|
|
u = MagicMock()
|
|
u.id = user_id
|
|
u.first_name = first_name
|
|
u.last_name = last_name
|
|
u.username = username
|
|
return u
|
|
|
|
|
|
def _make_chat(chat_type="private"):
|
|
"""Build a minimal chat."""
|
|
ch = MagicMock()
|
|
ch.type = chat_type
|
|
return ch
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_calls_get_or_create_user_and_reply_text():
|
|
"""start: calls get_or_create_user with expected fields and reply_text with start.greeting."""
|
|
message = MagicMock()
|
|
message.reply_text = AsyncMock()
|
|
user = _make_user(42, first_name="Alice", last_name="B")
|
|
update = _make_update(message=message, effective_user=user)
|
|
|
|
with patch(
|
|
"duty_teller.handlers.commands.session_scope",
|
|
) as mock_scope:
|
|
mock_session = MagicMock()
|
|
mock_scope.return_value.__enter__.return_value = mock_session
|
|
mock_scope.return_value.__exit__.return_value = None
|
|
with patch(
|
|
"duty_teller.handlers.commands.get_or_create_user",
|
|
) as mock_get_user:
|
|
with patch("duty_teller.handlers.commands.t") as mock_t:
|
|
mock_t.return_value = "Hi! Use /help."
|
|
with patch("duty_teller.handlers.commands.get_lang", return_value="en"):
|
|
await start(update, MagicMock())
|
|
mock_get_user.assert_called_once()
|
|
call_kw = mock_get_user.call_args[1]
|
|
assert call_kw["telegram_user_id"] == 42
|
|
assert call_kw["full_name"] == "Alice B"
|
|
assert call_kw["username"] == "testuser"
|
|
message.reply_text.assert_called_once_with("Hi! Use /help.")
|
|
mock_t.assert_called_with("en", "start.greeting")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_no_message_returns_early():
|
|
"""start: if update.message is None, does not call get_or_create_user or reply."""
|
|
update = _make_update(message=None, effective_user=_make_user())
|
|
with patch("duty_teller.handlers.commands.session_scope") as mock_scope:
|
|
with patch("duty_teller.handlers.commands.get_or_create_user") as mock_get_user:
|
|
await start(update, MagicMock())
|
|
mock_get_user.assert_not_called()
|
|
mock_scope.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_phone_private_with_number_replies_saved():
|
|
"""set_phone in private chat with number -> reply 'saved'."""
|
|
message = MagicMock()
|
|
message.reply_text = AsyncMock()
|
|
user = _make_user()
|
|
chat = _make_chat("private")
|
|
update = _make_update(message=message, effective_user=user, effective_chat=chat)
|
|
context = MagicMock()
|
|
context.args = ["+79001234567"]
|
|
|
|
with patch("duty_teller.handlers.commands.session_scope") as mock_scope:
|
|
mock_session = MagicMock()
|
|
mock_scope.return_value.__enter__.return_value = mock_session
|
|
mock_scope.return_value.__exit__.return_value = None
|
|
with patch("duty_teller.handlers.commands.get_or_create_user"):
|
|
with patch(
|
|
"duty_teller.handlers.commands.set_user_phone",
|
|
return_value=MagicMock(),
|
|
):
|
|
with patch("duty_teller.handlers.commands.get_lang", return_value="en"):
|
|
with patch("duty_teller.handlers.commands.t") as mock_t:
|
|
mock_t.return_value = "Phone saved: +79001234567"
|
|
await set_phone(update, context)
|
|
message.reply_text.assert_called_once_with("Phone saved: +79001234567")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_phone_private_no_number_replies_cleared():
|
|
"""set_phone in private chat without number -> reply 'cleared'."""
|
|
message = MagicMock()
|
|
message.reply_text = AsyncMock()
|
|
user = _make_user()
|
|
chat = _make_chat("private")
|
|
update = _make_update(message=message, effective_user=user, effective_chat=chat)
|
|
context = MagicMock()
|
|
context.args = []
|
|
|
|
with patch("duty_teller.handlers.commands.session_scope") as mock_scope:
|
|
mock_session = MagicMock()
|
|
mock_scope.return_value.__enter__.return_value = mock_session
|
|
mock_scope.return_value.__exit__.return_value = None
|
|
with patch("duty_teller.handlers.commands.get_or_create_user"):
|
|
with patch(
|
|
"duty_teller.handlers.commands.set_user_phone",
|
|
return_value=MagicMock(),
|
|
):
|
|
with patch("duty_teller.handlers.commands.get_lang", return_value="en"):
|
|
with patch("duty_teller.handlers.commands.t") as mock_t:
|
|
mock_t.return_value = "Phone cleared"
|
|
await set_phone(update, context)
|
|
message.reply_text.assert_called_once_with("Phone cleared")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_phone_group_replies_private_only():
|
|
"""set_phone in group chat -> reply private_only."""
|
|
message = MagicMock()
|
|
message.reply_text = AsyncMock()
|
|
user = _make_user()
|
|
chat = _make_chat("group")
|
|
update = _make_update(message=message, effective_user=user, effective_chat=chat)
|
|
context = MagicMock()
|
|
context.args = ["+79001234567"]
|
|
|
|
with patch("duty_teller.handlers.commands.get_lang", return_value="en"):
|
|
with patch("duty_teller.handlers.commands.t") as mock_t:
|
|
mock_t.return_value = "Private only"
|
|
await set_phone(update, context)
|
|
message.reply_text.assert_called_once_with("Private only")
|
|
mock_t.assert_called_with("en", "set_phone.private_only")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_calendar_link_with_user_and_token_replies_with_url():
|
|
"""calendar_link with allowed user and token -> reply with link."""
|
|
message = MagicMock()
|
|
message.reply_text = AsyncMock()
|
|
user = _make_user()
|
|
chat = _make_chat("private")
|
|
update = _make_update(message=message, effective_user=user, effective_chat=chat)
|
|
|
|
with patch("duty_teller.handlers.commands.session_scope") as mock_scope:
|
|
mock_session = MagicMock()
|
|
mock_scope.return_value.__enter__.return_value = mock_session
|
|
mock_scope.return_value.__exit__.return_value = None
|
|
with patch("duty_teller.handlers.commands.get_or_create_user") as mock_get_user:
|
|
mock_user = MagicMock()
|
|
mock_user.id = 10
|
|
mock_user.phone = None
|
|
mock_get_user.return_value = mock_user
|
|
with patch("duty_teller.handlers.commands.config") as mock_cfg:
|
|
mock_cfg.can_access_miniapp.return_value = True
|
|
mock_cfg.can_access_miniapp_by_phone.return_value = False
|
|
mock_cfg.MINI_APP_BASE_URL = "https://example.com"
|
|
with patch(
|
|
"duty_teller.handlers.commands.create_calendar_token",
|
|
return_value="abc43token",
|
|
):
|
|
with patch(
|
|
"duty_teller.handlers.commands.get_lang", return_value="en"
|
|
):
|
|
with patch("duty_teller.handlers.commands.t") as mock_t:
|
|
mock_t.side_effect = lambda lang, key, **kw: (
|
|
f"URL: {kw.get('url', '')}"
|
|
if "success" in key
|
|
else "Hint"
|
|
)
|
|
await calendar_link(update, MagicMock())
|
|
message.reply_text.assert_called_once()
|
|
call_args = message.reply_text.call_args[0][0]
|
|
assert "abc43token" in call_args or "example.com" in call_args
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_calendar_link_denied_replies_access_denied():
|
|
"""calendar_link when user not in allowlist -> reply access_denied."""
|
|
message = MagicMock()
|
|
message.reply_text = AsyncMock()
|
|
user = _make_user()
|
|
chat = _make_chat("private")
|
|
update = _make_update(message=message, effective_user=user, effective_chat=chat)
|
|
|
|
with patch("duty_teller.handlers.commands.session_scope") as mock_scope:
|
|
mock_session = MagicMock()
|
|
mock_scope.return_value.__enter__.return_value = mock_session
|
|
mock_scope.return_value.__exit__.return_value = None
|
|
with patch("duty_teller.handlers.commands.get_or_create_user") as mock_get_user:
|
|
mock_user = MagicMock()
|
|
mock_user.id = 10
|
|
mock_user.phone = None
|
|
mock_get_user.return_value = mock_user
|
|
with patch("duty_teller.handlers.commands.config") as mock_cfg:
|
|
mock_cfg.can_access_miniapp.return_value = False
|
|
mock_cfg.can_access_miniapp_by_phone.return_value = False
|
|
with patch("duty_teller.handlers.commands.get_lang", return_value="en"):
|
|
with patch("duty_teller.handlers.commands.t") as mock_t:
|
|
mock_t.return_value = "Access denied"
|
|
await calendar_link(update, MagicMock())
|
|
message.reply_text.assert_called_once_with("Access denied")
|
|
mock_t.assert_called_with("en", "calendar_link.access_denied")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_help_cmd_replies_with_help_text():
|
|
"""help_cmd: reply_text called with help content (title, start, help, etc.)."""
|
|
message = MagicMock()
|
|
message.reply_text = AsyncMock()
|
|
user = _make_user()
|
|
update = _make_update(message=message, effective_user=user)
|
|
|
|
with patch("duty_teller.handlers.commands.session_scope") as mock_scope:
|
|
mock_session = MagicMock()
|
|
mock_scope.return_value.__enter__.return_value = mock_session
|
|
mock_scope.return_value.__exit__.return_value = None
|
|
with patch(
|
|
"duty_teller.handlers.commands.is_admin_for_telegram_user",
|
|
return_value=False,
|
|
):
|
|
with patch("duty_teller.handlers.commands.get_lang", return_value="en"):
|
|
with patch("duty_teller.handlers.commands.t") as mock_t:
|
|
mock_t.side_effect = lambda lang, key: f"[{key}]"
|
|
await help_cmd(update, MagicMock())
|
|
message.reply_text.assert_called_once()
|
|
text = message.reply_text.call_args[0][0]
|
|
assert "help.title" in text or "[help.title]" in text
|
|
assert "help.start" in text or "[help.start]" in text
|