feat: add tests for admin checks 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 `is_admin_async` function to verify correct behavior based on user roles. - Added tests for error handling in the `error_handler` to ensure exceptions are logged and do not propagate. - Enhanced test coverage for configuration settings by adding a test for parsing admin phone numbers from environment variables. - Updated the `.cursorrules` file to include the use of a virtual environment for better dependency management.
This commit is contained in:
@@ -38,6 +38,13 @@ def test_can_access_miniapp_denied(monkeypatch):
|
||||
assert config.can_access_miniapp("") is False
|
||||
|
||||
|
||||
def test_settings_from_env_parse_admin_phones_adds_normalized(monkeypatch):
|
||||
"""Settings.from_env() parses ADMIN_PHONES and adds normalized phone to admin_phones."""
|
||||
monkeypatch.setenv("ADMIN_PHONES", "+7 900 123-45-67")
|
||||
settings = config.Settings.from_env()
|
||||
assert settings.admin_phones == {"79001234567"}
|
||||
|
||||
|
||||
def test_normalize_phone():
|
||||
"""Phone is normalized to digits only."""
|
||||
assert config.normalize_phone("+7 900 123-45-67") == "79001234567"
|
||||
|
||||
52
tests/test_handlers_common.py
Normal file
52
tests/test_handlers_common.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Tests for duty_teller.handlers.common (is_admin_async)."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from duty_teller.handlers.common import is_admin_async
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_admin_async_true_when_repository_returns_true():
|
||||
"""is_admin_async returns True when is_admin_for_telegram_user returns True."""
|
||||
mock_session = MagicMock()
|
||||
mock_scope = MagicMock()
|
||||
mock_scope.return_value.__enter__.return_value = mock_session
|
||||
mock_scope.return_value.__exit__.return_value = None
|
||||
|
||||
with patch(
|
||||
"duty_teller.handlers.common.session_scope",
|
||||
mock_scope,
|
||||
):
|
||||
with patch(
|
||||
"duty_teller.handlers.common.is_admin_for_telegram_user",
|
||||
return_value=True,
|
||||
) as mock_is_admin:
|
||||
result = await is_admin_async(12345)
|
||||
assert result is True
|
||||
mock_scope.assert_called_once()
|
||||
mock_scope.return_value.__enter__.assert_called_once()
|
||||
mock_is_admin.assert_called_once_with(mock_session, 12345)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_admin_async_false_when_repository_returns_false():
|
||||
"""is_admin_async returns False when is_admin_for_telegram_user returns False."""
|
||||
mock_session = MagicMock()
|
||||
mock_scope = MagicMock()
|
||||
mock_scope.return_value.__enter__.return_value = mock_session
|
||||
mock_scope.return_value.__exit__.return_value = None
|
||||
|
||||
with patch(
|
||||
"duty_teller.handlers.common.session_scope",
|
||||
mock_scope,
|
||||
):
|
||||
with patch(
|
||||
"duty_teller.handlers.common.is_admin_for_telegram_user",
|
||||
return_value=False,
|
||||
) as mock_is_admin:
|
||||
result = await is_admin_async(99999)
|
||||
assert result is False
|
||||
mock_scope.assert_called_once()
|
||||
mock_is_admin.assert_called_once_with(mock_session, 99999)
|
||||
@@ -52,3 +52,26 @@ async def test_error_handler_update_none_does_not_crash():
|
||||
context = MagicMock()
|
||||
context.error = Exception("test")
|
||||
await error_handler(None, context)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_error_handler_reply_text_raises_does_not_propagate():
|
||||
"""error_handler: when reply_text raises, exception is caught and logged; does not propagate."""
|
||||
class FakeUpdate:
|
||||
pass
|
||||
|
||||
update = FakeUpdate()
|
||||
update.effective_message = MagicMock()
|
||||
update.effective_message.reply_text = AsyncMock(
|
||||
side_effect=Exception("reply failed")
|
||||
)
|
||||
update.effective_user = MagicMock()
|
||||
context = MagicMock()
|
||||
context.error = Exception("test error")
|
||||
|
||||
with patch("duty_teller.handlers.errors.Update", FakeUpdate):
|
||||
with patch("duty_teller.handlers.errors.get_lang", return_value="en"):
|
||||
with patch("duty_teller.handlers.errors.t", return_value="An error occurred."):
|
||||
await error_handler(update, context)
|
||||
# Handler must not raise; reply_text was attempted
|
||||
update.effective_message.reply_text.assert_called_once()
|
||||
|
||||
@@ -58,3 +58,19 @@ def test_build_personal_ics_event_types():
|
||||
summaries = [str(ev.get("summary")) for ev in events]
|
||||
assert "Unavailable" in summaries
|
||||
assert "Vacation" in summaries
|
||||
|
||||
|
||||
def test_build_personal_ics_naive_datetime_treated_as_utc():
|
||||
"""Duties with start_at/end_at without Z (naive datetime) are treated as UTC."""
|
||||
duties = [
|
||||
_duty(1, "2025-02-20T09:00:00", "2025-02-20T18:00:00", "duty"),
|
||||
]
|
||||
ics = build_personal_ics(duties)
|
||||
cal = ICalendar.from_ical(ics)
|
||||
assert cal is not None
|
||||
events = [c for c in cal.walk() if c.name == "VEVENT"]
|
||||
assert len(events) == 1
|
||||
ev = events[0]
|
||||
assert ev.get("summary") == "Duty"
|
||||
assert "2025-02-20" in str(ev.get("dtstart").dt)
|
||||
assert "2025-02-20" in str(ev.get("dtend").dt)
|
||||
|
||||
Reference in New Issue
Block a user