Files
duty-teller/tests/test_handlers_commands.py
Nikolay Tatarinov aa89494bd5
All checks were successful
CI / lint-and-test (push) Successful in 22s
feat: enhance calendar ICS generation with event type filtering
- 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.
2026-02-20 17:47:52 +03:00

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