All checks were successful
CI / lint-and-test (push) Successful in 22s
- Added support for filtering calendar events by type in the ICS generation API endpoint, allowing users to specify whether to include only duty shifts or all event types (duty, unavailable, vacation). - Updated the `get_duties_for_user` function to accept an optional `event_types` parameter, enabling more flexible data retrieval based on user preferences. - Enhanced unit tests to cover the new event type filtering functionality, ensuring correct behavior and reliability of the ICS generation process.
120 lines
5.2 KiB
Python
120 lines
5.2 KiB
Python
"""Tests for duty_teller.run (main entry point with mocks)."""
|
|
|
|
import asyncio
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from duty_teller.run import main, _run_uvicorn, _set_default_menu_button_webapp
|
|
|
|
|
|
def test_main_builds_app_and_starts_thread():
|
|
"""main: calls require_bot_token, builds Application, register_handlers, starts uvicorn thread, run_polling."""
|
|
mock_app = MagicMock()
|
|
mock_app.run_polling = MagicMock(side_effect=KeyboardInterrupt())
|
|
mock_builder = MagicMock()
|
|
mock_builder.token.return_value = mock_builder
|
|
mock_builder.post_init.return_value = mock_builder
|
|
mock_builder.build.return_value = mock_app
|
|
|
|
# Avoid loading real DB when main() imports api.app (prevents unclosed connection warnings).
|
|
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.run.require_bot_token"):
|
|
with patch("duty_teller.run.ApplicationBuilder", return_value=mock_builder):
|
|
with patch("duty_teller.run.register_handlers") as mock_register:
|
|
with patch("duty_teller.run.threading.Thread") as mock_thread_class:
|
|
with patch("duty_teller.db.session.session_scope", mock_scope):
|
|
mock_thread = MagicMock()
|
|
mock_thread_class.return_value = mock_thread
|
|
with pytest.raises(KeyboardInterrupt):
|
|
main()
|
|
mock_register.assert_called_once_with(mock_app)
|
|
mock_builder.token.assert_called_once()
|
|
mock_thread.start.assert_called_once()
|
|
mock_app.run_polling.assert_called_once()
|
|
|
|
|
|
def test_run_uvicorn_creates_server():
|
|
"""_run_uvicorn: creates uvicorn config and runs server (mocked)."""
|
|
with patch("uvicorn.Server") as mock_server_class:
|
|
mock_server = MagicMock()
|
|
# run_until_complete needs a real coroutine
|
|
mock_server.serve = MagicMock(return_value=asyncio.sleep(0))
|
|
mock_server_class.return_value = mock_server
|
|
_run_uvicorn(MagicMock(), 8080)
|
|
mock_server_class.assert_called_once()
|
|
mock_server.serve.assert_called_once()
|
|
|
|
|
|
def test_set_default_menu_button_skips_when_no_base_url():
|
|
"""_set_default_menu_button_webapp: returns early when MINI_APP_BASE_URL or BOT_TOKEN not set."""
|
|
with patch("duty_teller.run.config") as mock_cfg:
|
|
mock_cfg.MINI_APP_BASE_URL = ""
|
|
mock_cfg.BOT_TOKEN = "token"
|
|
_set_default_menu_button_webapp()
|
|
mock_cfg.MINI_APP_BASE_URL = "https://example.com"
|
|
mock_cfg.BOT_TOKEN = ""
|
|
_set_default_menu_button_webapp()
|
|
# No urlopen should be called
|
|
with patch("duty_teller.run.config") as mock_cfg:
|
|
mock_cfg.MINI_APP_BASE_URL = None
|
|
mock_cfg.BOT_TOKEN = "x"
|
|
_set_default_menu_button_webapp()
|
|
|
|
|
|
def test_set_default_menu_button_skips_when_not_https():
|
|
"""_set_default_menu_button_webapp: returns early when menu_url does not start with https."""
|
|
with patch("duty_teller.run.config") as mock_cfg:
|
|
mock_cfg.MINI_APP_BASE_URL = "http://example.com"
|
|
mock_cfg.BOT_TOKEN = "token"
|
|
_set_default_menu_button_webapp()
|
|
# No urlopen
|
|
|
|
|
|
def test_set_default_menu_button_calls_telegram_api():
|
|
"""_set_default_menu_button_webapp: when URL is https, calls Telegram API (mocked)."""
|
|
with patch("duty_teller.run.config") as mock_cfg:
|
|
mock_cfg.MINI_APP_BASE_URL = "https://example.com/"
|
|
mock_cfg.BOT_TOKEN = "123:ABC"
|
|
with patch("urllib.request.urlopen") as mock_urlopen:
|
|
mock_resp = MagicMock()
|
|
mock_resp.status = 200
|
|
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
|
|
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
mock_urlopen.return_value = mock_resp
|
|
_set_default_menu_button_webapp()
|
|
mock_urlopen.assert_called_once()
|
|
req = mock_urlopen.call_args[0][0]
|
|
assert "setChatMenuButton" in req.full_url
|
|
assert "123:ABC" in req.full_url
|
|
|
|
|
|
def test_set_default_menu_button_handles_urlopen_error():
|
|
"""_set_default_menu_button_webapp: on urlopen error, logs and does not raise."""
|
|
with patch("duty_teller.run.config") as mock_cfg:
|
|
mock_cfg.MINI_APP_BASE_URL = "https://example.com/"
|
|
mock_cfg.BOT_TOKEN = "123:ABC"
|
|
with patch("urllib.request.urlopen") as mock_urlopen:
|
|
mock_urlopen.side_effect = OSError("network error")
|
|
_set_default_menu_button_webapp()
|
|
mock_urlopen.assert_called_once()
|
|
|
|
|
|
def test_set_default_menu_button_handles_non_200_response():
|
|
"""_set_default_menu_button_webapp: on non-200 response, logs warning."""
|
|
with patch("duty_teller.run.config") as mock_cfg:
|
|
mock_cfg.MINI_APP_BASE_URL = "https://example.com/"
|
|
mock_cfg.BOT_TOKEN = "123:ABC"
|
|
with patch("urllib.request.urlopen") as mock_urlopen:
|
|
mock_resp = MagicMock()
|
|
mock_resp.status = 400
|
|
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
|
|
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
mock_urlopen.return_value = mock_resp
|
|
_set_default_menu_button_webapp()
|
|
mock_urlopen.assert_called_once()
|