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