feat: enhance HTTP handling and configuration
All checks were successful
CI / lint-and-test (push) Successful in 24s

- Introduced a new utility function `safe_urlopen` to ensure only allowed URL schemes (http, https) are opened, enhancing security against path traversal vulnerabilities.
- Updated the `run.py` and `calendar_ics.py` files to utilize `safe_urlopen` for HTTP requests, improving error handling and security.
- Added `HTTP_HOST` configuration to the settings, allowing dynamic binding of the HTTP server host.
- Revised the `.env.example` file to include the new `HTTP_HOST` variable with a description.
- Enhanced tests for `safe_urlopen` to validate behavior with disallowed URL schemes and ensure proper integration in existing functionality.
This commit is contained in:
2026-02-24 14:16:34 +03:00
parent e6bc60b436
commit d5da265b5f
11 changed files with 150 additions and 19 deletions

View File

@@ -76,32 +76,32 @@ def test_set_default_menu_button_skips_when_not_https():
def test_set_default_menu_button_calls_telegram_api():
"""_set_default_menu_button_webapp: when URL is https, calls Telegram API (mocked)."""
"""_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("urllib.request.urlopen") as mock_urlopen:
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_urlopen.return_value = mock_resp
mock_safe_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
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 urlopen error, logs and does not raise."""
"""_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("urllib.request.urlopen") as mock_urlopen:
mock_urlopen.side_effect = OSError("network error")
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_urlopen.assert_called_once()
mock_safe_urlopen.assert_called_once()
def test_set_default_menu_button_handles_non_200_response():
@@ -109,11 +109,25 @@ def test_set_default_menu_button_handles_non_200_response():
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:
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_urlopen.return_value = mock_resp
mock_safe_urlopen.return_value = mock_resp
_set_default_menu_button_webapp()
mock_urlopen.assert_called_once()
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