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:
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
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 = 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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user