feat: add comprehensive tests for duty schedule import and error handling
All checks were successful
CI / lint-and-test (push) Successful in 23s
All checks were successful
CI / lint-and-test (push) Successful in 23s
- Introduced new tests for the `import_duty_schedule_cmd` to verify behavior when no message or effective user is present, ensuring proper early returns. - Added tests for admin checks to confirm that only authorized users can initiate duty schedule imports, enhancing security. - Implemented error handling tests for the `handle_handover_time_text` function to ensure appropriate responses for invalid time formats and non-admin users. - Enhanced overall test coverage for the duty schedule import functionality, contributing to improved reliability and maintainability of the codebase. - Updated the `.coverage` file to reflect the latest coverage metrics.
This commit is contained in:
@@ -9,6 +9,7 @@ from duty_teller.handlers.commands import (
|
||||
set_phone,
|
||||
calendar_link,
|
||||
help_cmd,
|
||||
set_role,
|
||||
)
|
||||
|
||||
|
||||
@@ -84,6 +85,20 @@ async def test_start_no_message_returns_early():
|
||||
mock_scope.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_no_effective_user_returns_early():
|
||||
"""start: if update.effective_user is None, does not call executor or reply."""
|
||||
message = MagicMock()
|
||||
message.reply_text = AsyncMock()
|
||||
update = _make_update(message=message, effective_user=None)
|
||||
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()
|
||||
message.reply_text.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'."""
|
||||
@@ -157,6 +172,34 @@ async def test_set_phone_group_replies_private_only():
|
||||
mock_t.assert_called_with("en", "set_phone.private_only")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_phone_result_error_replies_error():
|
||||
"""set_phone: set_user_phone returns None (error) -> reply set_phone.error."""
|
||||
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=None,
|
||||
):
|
||||
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 = "Error saving phone"
|
||||
await set_phone(update, context)
|
||||
message.reply_text.assert_called_once_with("Error saving phone")
|
||||
mock_t.assert_called_with("en", "set_phone.error")
|
||||
|
||||
|
||||
@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."""
|
||||
@@ -200,6 +243,60 @@ async def test_calendar_link_with_user_and_token_replies_with_url():
|
||||
assert "abc43token" in call_args or "example.com" in call_args
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_link_in_group_replies_private_only():
|
||||
"""calendar_link in group chat -> reply calendar_link.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)
|
||||
|
||||
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 chat only"
|
||||
await calendar_link(update, MagicMock())
|
||||
message.reply_text.assert_called_once_with("Private chat only")
|
||||
mock_t.assert_called_with("en", "calendar_link.private_only")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_link_no_url_replies_error():
|
||||
"""calendar_link: can_access True but MINI_APP_BASE_URL empty -> reply calendar_link.error."""
|
||||
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_get_user.return_value = mock_user
|
||||
with patch(
|
||||
"duty_teller.handlers.commands.can_access_miniapp_for_telegram_user",
|
||||
return_value=True,
|
||||
):
|
||||
with patch("duty_teller.handlers.commands.config") as mock_cfg:
|
||||
mock_cfg.MINI_APP_BASE_URL = ""
|
||||
with patch(
|
||||
"duty_teller.handlers.commands.create_calendar_token",
|
||||
return_value="token",
|
||||
):
|
||||
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 = "Calendar error"
|
||||
await calendar_link(update, MagicMock())
|
||||
message.reply_text.assert_called_once_with("Calendar error")
|
||||
mock_t.assert_called_with("en", "calendar_link.error")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_calendar_link_denied_replies_access_denied():
|
||||
"""calendar_link when user not in allowlist -> reply access_denied."""
|
||||
@@ -251,3 +348,217 @@ async def test_help_cmd_replies_with_help_text():
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_help_cmd_admin_includes_import_schedule_and_set_role():
|
||||
"""help_cmd: when is_admin_async True, reply includes help.import_schedule and help.set_role."""
|
||||
message = MagicMock()
|
||||
message.reply_text = AsyncMock()
|
||||
user = _make_user()
|
||||
update = _make_update(message=message, effective_user=user)
|
||||
|
||||
with patch(
|
||||
"duty_teller.handlers.commands.is_admin_async",
|
||||
new_callable=AsyncMock,
|
||||
return_value=True,
|
||||
):
|
||||
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.import_schedule" in text or "[help.import_schedule]" in text
|
||||
assert "help.set_role" in text or "[help.set_role]" in text
|
||||
|
||||
|
||||
# --- set_role ---
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_role_no_effective_user_returns_early():
|
||||
"""set_role: no update.effective_user -> no reply, no executor."""
|
||||
message = MagicMock()
|
||||
message.reply_text = AsyncMock()
|
||||
update = _make_update(message=message, effective_user=None)
|
||||
context = MagicMock()
|
||||
context.args = []
|
||||
with patch("duty_teller.handlers.commands.session_scope") as mock_scope:
|
||||
await set_role(update, context)
|
||||
message.reply_text.assert_not_called()
|
||||
mock_scope.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_role_not_admin_replies_admin_only():
|
||||
"""set_role: non-admin -> reply import.admin_only."""
|
||||
message = MagicMock()
|
||||
message.reply_text = AsyncMock()
|
||||
message.reply_to_message = None
|
||||
user = _make_user()
|
||||
update = _make_update(message=message, effective_user=user)
|
||||
context = MagicMock()
|
||||
context.args = []
|
||||
|
||||
with patch(
|
||||
"duty_teller.handlers.commands.is_admin_async",
|
||||
new_callable=AsyncMock,
|
||||
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 = "Admin only"
|
||||
await set_role(update, context)
|
||||
message.reply_text.assert_called_once_with("Admin only")
|
||||
mock_t.assert_called_with("en", "import.admin_only")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_role_invalid_usage_replies_usage():
|
||||
"""set_role: invalid role_name (not user|admin) -> reply set_role.usage."""
|
||||
message = MagicMock()
|
||||
message.reply_text = AsyncMock()
|
||||
message.reply_to_message = MagicMock()
|
||||
message.reply_to_message.from_user = MagicMock()
|
||||
message.reply_to_message.from_user.id = 111
|
||||
user = _make_user()
|
||||
update = _make_update(message=message, effective_user=user)
|
||||
context = MagicMock()
|
||||
context.args = ["invalid_role"]
|
||||
|
||||
with patch(
|
||||
"duty_teller.handlers.commands.is_admin_async",
|
||||
new_callable=AsyncMock,
|
||||
return_value=True,
|
||||
):
|
||||
with patch("duty_teller.handlers.commands.get_lang", return_value="en"):
|
||||
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_user_by_telegram_id",
|
||||
return_value=MagicMock(id=1, full_name="X"),
|
||||
):
|
||||
with patch("duty_teller.handlers.commands.t") as mock_t:
|
||||
mock_t.return_value = "Usage: ..."
|
||||
await set_role(update, context)
|
||||
message.reply_text.assert_called_once_with("Usage: ...")
|
||||
mock_t.assert_called_with("en", "set_role.usage")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_role_user_not_found_replies_user_not_found():
|
||||
"""set_role: target user not found -> reply set_role.user_not_found."""
|
||||
message = MagicMock()
|
||||
message.reply_text = AsyncMock()
|
||||
message.reply_to_message = MagicMock()
|
||||
message.reply_to_message.from_user = MagicMock()
|
||||
message.reply_to_message.from_user.id = 999
|
||||
user = _make_user()
|
||||
update = _make_update(message=message, effective_user=user)
|
||||
context = MagicMock()
|
||||
context.args = ["admin"]
|
||||
|
||||
with patch(
|
||||
"duty_teller.handlers.commands.is_admin_async",
|
||||
new_callable=AsyncMock,
|
||||
return_value=True,
|
||||
):
|
||||
with patch("duty_teller.handlers.commands.get_lang", return_value="en"):
|
||||
with patch(
|
||||
"duty_teller.handlers.commands.get_user_by_telegram_id",
|
||||
return_value=None,
|
||||
):
|
||||
with patch("duty_teller.handlers.commands.t") as mock_t:
|
||||
mock_t.return_value = "User not found"
|
||||
await set_role(update, context)
|
||||
message.reply_text.assert_called_once_with("User not found")
|
||||
mock_t.assert_called_with("en", "set_role.user_not_found")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_role_success_reply_replies_done():
|
||||
"""set_role: reply to user + role -> set_user_role success -> reply set_role.done."""
|
||||
message = MagicMock()
|
||||
message.reply_text = AsyncMock()
|
||||
message.reply_to_message = MagicMock()
|
||||
message.reply_to_message.from_user = MagicMock()
|
||||
message.reply_to_message.from_user.id = 888
|
||||
user = _make_user()
|
||||
update = _make_update(message=message, effective_user=user)
|
||||
context = MagicMock()
|
||||
context.args = ["admin"]
|
||||
|
||||
mock_target = MagicMock()
|
||||
mock_target.id = 10
|
||||
mock_target.full_name = "Target User"
|
||||
|
||||
with patch(
|
||||
"duty_teller.handlers.commands.is_admin_async",
|
||||
new_callable=AsyncMock,
|
||||
return_value=True,
|
||||
):
|
||||
with patch("duty_teller.handlers.commands.get_lang", return_value="en"):
|
||||
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_user_by_telegram_id",
|
||||
return_value=mock_target,
|
||||
):
|
||||
with patch(
|
||||
"duty_teller.handlers.commands.set_user_role",
|
||||
return_value=mock_target,
|
||||
):
|
||||
with patch("duty_teller.handlers.commands.t") as mock_t:
|
||||
mock_t.return_value = "Role set"
|
||||
await set_role(update, context)
|
||||
message.reply_text.assert_called_once_with("Role set")
|
||||
mock_t.assert_called_with("en", "set_role.done", name="Target User", role="admin")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_role_db_error_replies_error():
|
||||
"""set_role: set_user_role returns None -> reply set_role.error."""
|
||||
message = MagicMock()
|
||||
message.reply_text = AsyncMock()
|
||||
message.reply_to_message = MagicMock()
|
||||
message.reply_to_message.from_user = MagicMock()
|
||||
message.reply_to_message.from_user.id = 777
|
||||
user = _make_user()
|
||||
update = _make_update(message=message, effective_user=user)
|
||||
context = MagicMock()
|
||||
context.args = ["user"]
|
||||
|
||||
mock_target = MagicMock()
|
||||
mock_target.id = 5
|
||||
mock_target.full_name = "Someone"
|
||||
|
||||
with patch(
|
||||
"duty_teller.handlers.commands.is_admin_async",
|
||||
new_callable=AsyncMock,
|
||||
return_value=True,
|
||||
):
|
||||
with patch("duty_teller.handlers.commands.get_lang", return_value="en"):
|
||||
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_user_by_telegram_id",
|
||||
return_value=mock_target,
|
||||
):
|
||||
with patch(
|
||||
"duty_teller.handlers.commands.set_user_role",
|
||||
return_value=None,
|
||||
):
|
||||
with patch("duty_teller.handlers.commands.t") as mock_t:
|
||||
mock_t.return_value = "Error"
|
||||
await set_role(update, context)
|
||||
message.reply_text.assert_called_once_with("Error")
|
||||
mock_t.assert_called_with("en", "set_role.error")
|
||||
|
||||
Reference in New Issue
Block a user