From f9c4bac7b806a1656041e915e544bac6a061a105 Mon Sep 17 00:00:00 2001 From: shoko <270575765+shokollm@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:07:10 +0000 Subject: [PATCH] Refactor send() to module-level send_telegram_message() for testability Extract the nested send() function into a module-level send_telegram_message(bot_token, chat_id, text, timeout=10) function. This enables unit testing without hitting the real Telegram API. Changes: - Add send_telegram_message() at module level in TELEGRAM section - Replace nested send() with thin wrapper that calls send_telegram_message() - Update argparse --telegram help text to use TELEGRAM_BOT_TOKEN - Add tests/test_browse.py with 8 unit tests covering: - Success case (returns message_id) - API error (RuntimeError) - Invalid token (HTTPError 404) - Rate limit (HTTPError 429) - Network error (URLError) - Timeout (URLError) - Custom timeout parameter - HTML parse_mode in request Ref: #4 --- .gitignore | 1 + skills/polymarket-browse/scripts/browse.py | 39 ++++-- skills/polymarket-browse/tests/__init__.py | 1 + skills/polymarket-browse/tests/test_browse.py | 125 ++++++++++++++++++ 4 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 skills/polymarket-browse/tests/__init__.py create mode 100644 skills/polymarket-browse/tests/test_browse.py diff --git a/.gitignore b/.gitignore index c3b31bf..f51db2e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ __pycache__/ *.pyc *.pyo .DS_Store +.worktrees/ diff --git a/skills/polymarket-browse/scripts/browse.py b/skills/polymarket-browse/scripts/browse.py index b808d58..8ce83e0 100644 --- a/skills/polymarket-browse/scripts/browse.py +++ b/skills/polymarket-browse/scripts/browse.py @@ -577,6 +577,28 @@ def print_detail(e, detail): # TELEGRAM # ============================================================ +def send_telegram_message(bot_token, chat_id, text, timeout=10): + """Send a message via Telegram bot API. Returns the message ID on success. + + Raises: + RuntimeError: If the Telegram API returns an error (e.g. invalid token, rate limit). + URLError/HTTPError: On network or HTTP-level failures. + """ + url = f"https://api.telegram.org/bot{bot_token}/sendMessage" + data = urlencode({ + "chat_id": chat_id, + "text": text, + "parse_mode": "HTML", + "disable_web_page_preview": "true", + }).encode("utf-8") + req = Request(url, data=data, method="POST") + with urlopen(req, timeout=timeout) as resp: + result = json.loads(resp.read()) + if not result.get("ok"): + raise RuntimeError(f"Telegram API error: {result.get('description')}") + return result["result"]["message_id"] + + def send_to_telegram(match_events, non_match_events, category, matches_only=False, non_matches_only=False): """Send browse results to Telegram. Reads TELEGRAM_BOT_TOKEN and CHAT_ID from environment.""" import os @@ -596,19 +618,8 @@ def send_to_telegram(match_events, non_match_events, category, matches_only=Fals show_non_matches = (not matches_only and not non_matches_only) or non_matches_only def send(text): - url = f"https://api.telegram.org/bot{bot_token}/sendMessage" - data = urlencode({ - "chat_id": chat_id, - "text": text, - "parse_mode": "HTML", - "disable_web_page_preview": "true", - }).encode("utf-8") - req = Request(url, data=data, method="POST") - with urlopen(req, timeout=10) as resp: - result = json.loads(resp.read()) - if not result.get("ok"): - raise RuntimeError(f"Telegram API error: {result.get('description')}") - print(f" Sent msg {result['result']['message_id']}") + msg_id = send_telegram_message(bot_token, chat_id, text) + print(f" Sent msg {msg_id}") # Build sections lines = [f"{category.upper()} | {header_date}"] @@ -737,7 +748,7 @@ def main(): parser.add_argument("--raw", action="store_true", help="Show all events without tradeable filter (for debugging).") parser.add_argument("--telegram", action="store_true", - help="Send results to Telegram (BOT_TOKEN and CHAT_ID must be set in environment).") + help="Send results to Telegram (TELEGRAM_BOT_TOKEN and CHAT_ID must be set in environment).") args = parser.parse_args() if args.list_categories: diff --git a/skills/polymarket-browse/tests/__init__.py b/skills/polymarket-browse/tests/__init__.py new file mode 100644 index 0000000..d4839a6 --- /dev/null +++ b/skills/polymarket-browse/tests/__init__.py @@ -0,0 +1 @@ +# Tests package diff --git a/skills/polymarket-browse/tests/test_browse.py b/skills/polymarket-browse/tests/test_browse.py new file mode 100644 index 0000000..7b5b8a5 --- /dev/null +++ b/skills/polymarket-browse/tests/test_browse.py @@ -0,0 +1,125 @@ +""" +Unit tests for browse.py Telegram functions. + +Run with: python -m pytest tests/test_browse.py -v +""" + +import unittest +from unittest.mock import patch, MagicMock +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts')) +from browse import send_telegram_message + + +class TestSendTelegramMessage(unittest.TestCase): + """Tests for the module-level send_telegram_message function.""" + + @patch('browse.urlopen') + def test_send_success(self, mock_urlopen): + """send_telegram_message returns message_id on success.""" + mock_resp = MagicMock() + mock_resp.read.return_value = b'{"ok": true, "result": {"message_id": 123}}' + mock_urlopen.return_value.__enter__.return_value = mock_resp + + result = send_telegram_message("test_token", "test_chat", "hello world") + + self.assertEqual(result, 123) + mock_urlopen.assert_called_once() + call_args = mock_urlopen.call_args + req = call_args[0][0] + self.assertEqual(req.full_url, "https://api.telegram.org/bottest_token/sendMessage") + self.assertEqual(req.method, "POST") + + @patch('browse.urlopen') + def test_send_api_error_raises_runtime_error(self, mock_urlopen): + """send_telegram_message raises RuntimeError when Telegram API returns ok=false.""" + mock_resp = MagicMock() + mock_resp.read.return_value = b'{"ok": false, "description": "Forbidden"}' + mock_urlopen.return_value.__enter__.return_value = mock_resp + + with self.assertRaises(RuntimeError) as ctx: + send_telegram_message("test_token", "test_chat", "hello") + self.assertIn("Telegram API error: Forbidden", str(ctx.exception)) + + @patch('browse.urlopen') + def test_send_invalid_token_raises_http_error(self, mock_urlopen): + """send_telegram_message raises HTTPError on invalid token (404).""" + from urllib.error import HTTPError + mock_urlopen.side_effect = HTTPError( + url="https://api.telegram.org/botINVALID/sendMessage", + code=404, + msg="Not Found", + hdrs={}, + fp=None + ) + + with self.assertRaises(HTTPError) as ctx: + send_telegram_message("INVALID", "test_chat", "hello") + self.assertEqual(ctx.exception.code, 404) + + @patch('browse.urlopen') + def test_send_rate_limit_raises_http_error(self, mock_urlopen): + """send_telegram_message raises HTTPError on rate limit (429).""" + from urllib.error import HTTPError + mock_urlopen.side_effect = HTTPError( + url="https://api.telegram.org/bottest_token/sendMessage", + code=429, + msg="Too Many Requests", + hdrs={}, + fp=None + ) + + with self.assertRaises(HTTPError) as ctx: + send_telegram_message("test_token", "test_chat", "hello") + self.assertEqual(ctx.exception.code, 429) + + @patch('browse.urlopen') + def test_send_network_error_raises_url_error(self, mock_urlopen): + """send_telegram_message raises URLError on network failure.""" + from urllib.error import URLError + mock_urlopen.side_effect = URLError("Connection refused") + + with self.assertRaises(URLError) as ctx: + send_telegram_message("test_token", "test_chat", "hello") + self.assertIn("Connection refused", str(ctx.exception)) + + @patch('browse.urlopen') + def test_send_timeout_raises_url_error(self, mock_urlopen): + """send_telegram_message raises URLError on timeout.""" + from urllib.error import URLError + mock_urlopen.side_effect = URLError("") + + with self.assertRaises(URLError): + send_telegram_message("test_token", "test_chat", "hello") + + @patch('browse.urlopen') + def test_send_custom_timeout_used(self, mock_urlopen): + """send_telegram_message respects custom timeout parameter.""" + mock_resp = MagicMock() + mock_resp.read.return_value = b'{"ok": true, "result": {"message_id": 456}}' + mock_urlopen.return_value.__enter__.return_value = mock_resp + + send_telegram_message("test_token", "test_chat", "hello", timeout=30) + + call_kwargs = mock_urlopen.call_args[1] + self.assertEqual(call_kwargs['timeout'], 30) + + @patch('browse.urlopen') + def test_send_html_parsing_mode(self, mock_urlopen): + """send_telegram_message sends with parse_mode=HTML.""" + mock_resp = MagicMock() + mock_resp.read.return_value = b'{"ok": true, "result": {"message_id": 789}}' + mock_urlopen.return_value.__enter__.return_value = mock_resp + + send_telegram_message("test_token", "test_chat", "bold") + + call_args = mock_urlopen.call_args + req = call_args[0][0] + # Verify parse_mode=HTML is in the data + self.assertIn(b"parse_mode=HTML", req.data) + + +if __name__ == "__main__": + unittest.main() -- 2.49.1