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.
57 lines
2.2 KiB
Python
57 lines
2.2 KiB
Python
"""Tests for duty_teller.utils.http_client (safe_urlopen scheme check)."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from urllib.request import Request
|
|
|
|
from duty_teller.utils.http_client import safe_urlopen
|
|
|
|
|
|
def test_safe_urlopen_rejects_file_scheme():
|
|
"""safe_urlopen: file:// URL raises ValueError and does not call urlopen."""
|
|
req = Request("file:///etc/passwd")
|
|
with patch("duty_teller.utils.http_client.urlopen") as mock_urlopen:
|
|
with pytest.raises(ValueError, match="scheme not allowed"):
|
|
with safe_urlopen(req, timeout=5):
|
|
pass
|
|
mock_urlopen.assert_not_called()
|
|
|
|
|
|
def test_safe_urlopen_rejects_ftp_scheme():
|
|
"""safe_urlopen: ftp:// URL raises ValueError and does not call urlopen."""
|
|
req = Request("ftp://example.com/file.txt")
|
|
with patch("duty_teller.utils.http_client.urlopen") as mock_urlopen:
|
|
with pytest.raises(ValueError, match="scheme not allowed"):
|
|
with safe_urlopen(req, timeout=5):
|
|
pass
|
|
mock_urlopen.assert_not_called()
|
|
|
|
|
|
def test_safe_urlopen_allows_https():
|
|
"""safe_urlopen: https:// URL is allowed and urlopen is called."""
|
|
req = Request("https://api.telegram.org/bot123/setChatMenuButton")
|
|
mock_resp = MagicMock()
|
|
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
|
|
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
with patch(
|
|
"duty_teller.utils.http_client.urlopen", return_value=mock_resp
|
|
) as mock_urlopen:
|
|
with safe_urlopen(req, timeout=10) as resp:
|
|
assert resp is mock_resp
|
|
mock_urlopen.assert_called_once_with(req, timeout=10)
|
|
|
|
|
|
def test_safe_urlopen_allows_http_when_in_allowed_schemes():
|
|
"""safe_urlopen: http:// URL is allowed when allowed_schemes includes http."""
|
|
req = Request("http://localhost:8080/health")
|
|
mock_resp = MagicMock()
|
|
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
|
|
mock_resp.__exit__ = MagicMock(return_value=False)
|
|
with patch(
|
|
"duty_teller.utils.http_client.urlopen", return_value=mock_resp
|
|
) as mock_urlopen:
|
|
with safe_urlopen(req, timeout=5, allowed_schemes=("https", "http")) as resp:
|
|
assert resp is mock_resp
|
|
mock_urlopen.assert_called_once_with(req, timeout=5)
|