feat: add tests for admin checks and error handling
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:
2026-02-21 00:50:29 +03:00
parent bf8ac87a73
commit 35946a5812
6 changed files with 99 additions and 0 deletions

BIN
.coverage

Binary file not shown.

View File

@@ -105,6 +105,7 @@
"styling": "Not applicable", "styling": "Not applicable",
"testing_framework": "pytest", "testing_framework": "pytest",
"build_tool": "setuptools", "build_tool": "setuptools",
"use_venv": ".venv",
"deployment": { "deployment": {
"environment": "Local machine", "environment": "Local machine",

View File

@@ -38,6 +38,13 @@ def test_can_access_miniapp_denied(monkeypatch):
assert config.can_access_miniapp("") is False 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(): def test_normalize_phone():
"""Phone is normalized to digits only.""" """Phone is normalized to digits only."""
assert config.normalize_phone("+7 900 123-45-67") == "79001234567" assert config.normalize_phone("+7 900 123-45-67") == "79001234567"

View 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)

View File

@@ -52,3 +52,26 @@ async def test_error_handler_update_none_does_not_crash():
context = MagicMock() context = MagicMock()
context.error = Exception("test") context.error = Exception("test")
await error_handler(None, context) 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()

View File

@@ -58,3 +58,19 @@ def test_build_personal_ics_event_types():
summaries = [str(ev.get("summary")) for ev in events] summaries = [str(ev.get("summary")) for ev in events]
assert "Unavailable" in summaries assert "Unavailable" in summaries
assert "Vacation" 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)