"""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.config") as mock_cfg: mock_cfg.MINI_APP_SKIP_AUTH = False mock_cfg.HTTP_HOST = "127.0.0.1" mock_cfg.HTTP_PORT = 8080 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.run._wait_for_http_ready", return_value=True ): 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 safe_urlopen (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("duty_teller.run.safe_urlopen") as mock_safe_urlopen: mock_resp = MagicMock() mock_resp.status = 200 mock_resp.__enter__ = MagicMock(return_value=mock_resp) mock_resp.__exit__ = MagicMock(return_value=False) mock_safe_urlopen.return_value = mock_resp _set_default_menu_button_webapp() mock_safe_urlopen.assert_called_once() req = mock_safe_urlopen.call_args[0][0] assert "setChatMenuButton" in req.get_full_url() assert "123:ABC" in req.get_full_url() def test_set_default_menu_button_handles_urlopen_error(): """_set_default_menu_button_webapp: on safe_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("duty_teller.run.safe_urlopen") as mock_safe_urlopen: mock_safe_urlopen.side_effect = OSError("network error") _set_default_menu_button_webapp() mock_safe_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("duty_teller.run.safe_urlopen") as mock_safe_urlopen: mock_resp = MagicMock() mock_resp.status = 400 mock_resp.__enter__ = MagicMock(return_value=mock_resp) mock_resp.__exit__ = MagicMock(return_value=False) mock_safe_urlopen.return_value = mock_resp _set_default_menu_button_webapp() mock_safe_urlopen.assert_called_once() def test_run_uvicorn_uses_config_host(): """_run_uvicorn: passes config.HTTP_HOST to uvicorn.Config.""" with patch("uvicorn.Server") as mock_server_class: mock_server = MagicMock() mock_server.serve = MagicMock(return_value=asyncio.sleep(0)) mock_server_class.return_value = mock_server with patch("duty_teller.run.config") as mock_cfg: mock_cfg.HTTP_HOST = "127.0.0.1" _run_uvicorn(MagicMock(), 8080) call_kwargs = mock_server_class.call_args[0][0] assert call_kwargs.host == "127.0.0.1" assert call_kwargs.port == 8080