Some checks failed
CI / lint-and-test (push) Failing after 11s
- Upgraded `pytest-asyncio` to version 1.0 to ensure compatibility with the latest features and improvements. - Increased the coverage threshold in pytest configuration to 80%, enhancing the quality assurance process. - Added a new `conftest.py` file to manage shared fixtures and improve test organization. - Introduced multiple new test files to cover various components, ensuring comprehensive test coverage across the application. - Updated the `.coverage` file to reflect the latest coverage metrics.
250 lines
10 KiB
Python
250 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
|