Files
wehub-resource-sync 4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

403 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for utils/telegram_delivery.py."""
from __future__ import annotations
import logging
from typing import Any
from unittest.mock import MagicMock
import pytest
from integrations.telegram import delivery as telegram_delivery
from integrations.telegram.delivery import (
_TelegramTokenFilter,
post_telegram_message,
send_telegram_report,
truncate_for_telegram_html,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _mock_response(status_code: int, body: dict[str, Any]) -> MagicMock:
resp = MagicMock()
resp.status_code = status_code
resp.json.return_value = body
return resp
# ---------------------------------------------------------------------------
# post_telegram_message
# ---------------------------------------------------------------------------
def test_post_telegram_message_success(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"platform.notifications.delivery_transport.httpx.post",
lambda *_a, **_kw: _mock_response(200, {"ok": True, "result": {"message_id": 42}}),
)
ok, error, message_id = post_telegram_message("chat-1", "hello", "bot-token")
assert ok is True
assert error == ""
assert message_id == "42"
def test_post_telegram_message_sends_correct_payload(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
def _fake_post(url: str, *, json: dict[str, Any], **_kw: Any) -> MagicMock:
captured["url"] = url
captured["json"] = json
return _mock_response(200, {"ok": True, "result": {"message_id": 1}})
monkeypatch.setattr("platform.notifications.delivery_transport.httpx.post", _fake_post)
post_telegram_message("chat-42", "test text", "my-token")
assert "my-token" in captured["url"]
assert "sendMessage" in captured["url"]
assert captured["json"]["chat_id"] == "chat-42"
assert captured["json"]["text"] == "test text"
def test_post_telegram_message_with_reply_to(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
def _fake_post(url: str, *, json: dict[str, Any], **_kw: Any) -> MagicMock:
captured["json"] = json
return _mock_response(200, {"ok": True, "result": {"message_id": 2}})
monkeypatch.setattr("platform.notifications.delivery_transport.httpx.post", _fake_post)
post_telegram_message("chat-1", "text", "token", reply_to_message_id="99")
assert captured["json"]["reply_to_message_id"] == 99
def test_post_telegram_message_api_error(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"platform.notifications.delivery_transport.httpx.post",
lambda *_a, **_kw: _mock_response(
400, {"ok": False, "description": "Bad Request: chat not found"}
),
)
ok, error, message_id = post_telegram_message("chat-1", "text", "bot-token")
assert ok is False
assert "Bad Request" in error
assert message_id == ""
def test_post_telegram_message_exception_returns_false(monkeypatch: pytest.MonkeyPatch) -> None:
def _raise(*_a: Any, **_kw: Any) -> None:
raise ConnectionError("network down")
monkeypatch.setattr("platform.notifications.delivery_transport.httpx.post", _raise)
ok, error, message_id = post_telegram_message("chat-1", "text", "bot-token")
assert ok is False
assert "network down" in error
assert message_id == ""
def test_post_telegram_message_exception_redacts_token(monkeypatch: pytest.MonkeyPatch) -> None:
secret = "secret-bot-token-123"
def _raise(*_a: Any, **_kw: Any) -> None:
raise ConnectionError(f"failed to connect to api.telegram.org/bot{secret}/sendMessage")
monkeypatch.setattr("platform.notifications.delivery_transport.httpx.post", _raise)
ok, error, _ = post_telegram_message("chat-1", "text", secret)
assert ok is False
assert secret not in error
assert "<redacted>" in error
# ---------------------------------------------------------------------------
# send_telegram_report
# ---------------------------------------------------------------------------
def test_send_telegram_report_posts_to_chat(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
def _fake_post(url: str, *, json: dict[str, Any], **_kw: Any) -> MagicMock:
captured["url"] = url
captured["json"] = json
return _mock_response(200, {"ok": True, "result": {"message_id": 5}})
monkeypatch.setattr("platform.notifications.delivery_transport.httpx.post", _fake_post)
ok, error = send_telegram_report("Report text", {"bot_token": "tok", "chat_id": "chat-1"})
assert ok is True
assert error == ""
assert "tok" in captured["url"]
assert captured["json"]["chat_id"] == "chat-1"
assert captured["json"]["text"] == "Report text"
assert captured["json"]["parse_mode"] == "HTML"
def test_send_telegram_report_uses_reply_to_message_id(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
def _fake_post(url: str, *, json: dict[str, Any], **_kw: Any) -> MagicMock:
captured["json"] = json
return _mock_response(200, {"ok": True, "result": {"message_id": 6}})
monkeypatch.setattr("platform.notifications.delivery_transport.httpx.post", _fake_post)
send_telegram_report(
"Report",
{"bot_token": "tok", "chat_id": "chat-1", "reply_to_message_id": "77"},
)
assert captured["json"].get("reply_to_message_id") == 77
def test_send_telegram_report_passes_reply_markup(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
def _fake_post(url: str, *, json: dict[str, Any], **_kw: Any) -> MagicMock:
captured["json"] = json
return _mock_response(200, {"ok": True, "result": {"message_id": 7}})
monkeypatch.setattr("platform.notifications.delivery_transport.httpx.post", _fake_post)
markup = {"inline_keyboard": [[{"text": "Open", "url": "https://x"}]]}
ok, err = send_telegram_report(
"Hi",
{"bot_token": "tok", "chat_id": "c1"},
reply_markup=markup,
)
assert ok is True
assert err == ""
assert captured["json"]["reply_markup"] == markup
def test_send_telegram_report_returns_false_on_api_error(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"platform.notifications.delivery_transport.httpx.post",
lambda *_a, **_kw: _mock_response(403, {"ok": False, "description": "Forbidden"}),
)
ok, error = send_telegram_report("Report", {"bot_token": "tok", "chat_id": "chat-1"})
assert ok is False
assert "Forbidden" in error
# ---------------------------------------------------------------------------
# fix 1 httpx log token filter
# ---------------------------------------------------------------------------
def test_telegram_token_filter_scrubs_url_from_msg() -> None:
f = _TelegramTokenFilter()
record = logging.LogRecord(
name="httpx",
level=logging.INFO,
pathname="",
lineno=0,
msg="HTTP Request: POST https://api.telegram.org/botSECRET123/sendMessage",
args=(),
exc_info=None,
)
f.filter(record)
assert "SECRET123" not in record.msg
assert "bot<redacted>/sendMessage" in record.msg
def test_telegram_token_filter_scrubs_token_in_args() -> None:
f = _TelegramTokenFilter()
record = logging.LogRecord(
name="httpcore",
level=logging.DEBUG,
pathname="",
lineno=0,
msg="send %s",
args=("https://api.telegram.org/botSECRET123/sendMessage",),
exc_info=None,
)
f.filter(record)
assert isinstance(record.args, tuple)
assert "SECRET123" not in record.args[0]
assert "bot<redacted>/sendMessage" in record.args[0]
def test_telegram_token_filter_scrubs_non_string_url_arg() -> None:
"""httpx passes request.url as an httpx.URL object, not a plain str."""
class FakeURL:
def __str__(self) -> str:
return "https://api.telegram.org/botSECRET123/sendMessage"
f = _TelegramTokenFilter()
record = logging.LogRecord(
name="httpx",
level=logging.INFO,
pathname="",
lineno=0,
msg='HTTP Request: %s %s "%s %d %s"',
args=("POST", FakeURL(), "HTTP/1.1", 200, "OK"),
exc_info=None,
)
f.filter(record)
assert isinstance(record.args, tuple)
assert "SECRET123" not in str(record.args[1])
assert "bot<redacted>/sendMessage" in str(record.args[1])
# ---------------------------------------------------------------------------
# fix 2 send_telegram_report guards missing creds
# ---------------------------------------------------------------------------
def test_send_telegram_report_missing_bot_token() -> None:
ok, error = send_telegram_report("report", {"bot_token": "", "chat_id": "chat-1"})
assert ok is False
assert "Missing" in error
def test_send_telegram_report_missing_chat_id() -> None:
ok, error = send_telegram_report("report", {"bot_token": "tok", "chat_id": ""})
assert ok is False
assert "Missing" in error
def test_send_telegram_report_missing_both_creds() -> None:
ok, error = send_telegram_report("report", {})
assert ok is False
assert "Missing" in error
# ---------------------------------------------------------------------------
# fix 3 resp.json() only called after status check (non-JSON error body)
# ---------------------------------------------------------------------------
def test_post_telegram_message_non_json_error_body(monkeypatch: pytest.MonkeyPatch) -> None:
resp = MagicMock()
resp.status_code = 502
resp.json.side_effect = ValueError("not JSON")
resp.text = "Bad Gateway"
monkeypatch.setattr(
"platform.notifications.delivery_transport.httpx.post", lambda *_a, **_kw: resp
)
ok, error, message_id = post_telegram_message("chat-1", "text", "tok")
assert ok is False
assert "Bad Gateway" in error
assert message_id == ""
# ---------------------------------------------------------------------------
# fix 4 reply_to_message_id="0" must not be sent to the API
# ---------------------------------------------------------------------------
def test_post_telegram_message_reply_to_zero_string_not_sent(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, Any] = {}
def _fake_post(url: str, *, json: dict[str, Any], **_kw: Any) -> MagicMock:
captured["json"] = json
return _mock_response(200, {"ok": True, "result": {"message_id": 1}})
monkeypatch.setattr("platform.notifications.delivery_transport.httpx.post", _fake_post)
post_telegram_message("chat-1", "text", "tok", reply_to_message_id="0")
assert "reply_to_message_id" not in captured["json"]
def test_send_telegram_report_truncates_to_4096(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, Any] = {}
monkeypatch.setattr(
"platform.notifications.delivery_transport.httpx.post",
lambda *_a, **kw: (
captured.update({"text": kw["json"].get("text", "")})
or _mock_response(200, {"ok": True, "result": {"message_id": 7}})
), # type: ignore[misc]
)
long_report = "x" * 5000
send_telegram_report(long_report, {"bot_token": "tok", "chat_id": "chat-1"})
assert len(captured["text"]) == 4096
assert captured["text"].endswith("…")
def test_truncate_for_telegram_html_strips_incomplete_trailing_tag() -> None:
# Narrow limit so slice lands after ``<b>`` opens but well before ``</b>``.
src = "<b>" + ("m" * 80)
out = truncate_for_telegram_html(src, 18, suffix="…")
assert len(out) <= 18
assert out.endswith("…")
assert not out[:-1].replace("…", "").endswith("<")
def test_truncate_for_telegram_html_balances_open_tags_within_limit() -> None:
long_inner = "z" * 6000
src = f"<b>start {long_inner}</b>"
out = truncate_for_telegram_html(src, 4096, suffix="…")
assert len(out) == 4096
assert out.endswith("…")
assert out.count("<b>") == out.count("</b>")
def test_truncate_for_telegram_html_noop_when_under_limit() -> None:
s = "<i>ok</i>"
assert truncate_for_telegram_html(s, 50) == s
# ---------------------------------------------------------------------------
# Shared-transport delegation (regression coverage for the #864 refactor)
# ---------------------------------------------------------------------------
class TestDelegatesToSharedTransport:
"""After #864 the telegram helper uses ``delivery_transport.post_json``
rather than calling httpx directly. Pins the contract so re-importing
httpx into ``telegram_delivery`` regresses loudly."""
def test_module_does_not_import_httpx(self) -> None:
# Reuse the module-level ``from integrations.telegram_delivery import delivery as telegram_delivery`` (renamed for cleanliness)
# to avoid importing the same module via both ``import`` and
# ``from import`` styles (CodeQL py/import-and-import-from).
assert not hasattr(telegram_delivery, "httpx"), (
"telegram_delivery should not import httpx directly — "
"it must go through delivery_transport.post_json"
)
def test_post_message_uses_post_json_helper(self, monkeypatch: pytest.MonkeyPatch) -> None:
from platform.notifications.delivery_transport import DeliveryResponse
captured: dict[str, Any] = {}
def _stub_post_json(url: str, payload: dict, **kw: Any) -> DeliveryResponse:
captured["url"] = url
captured["payload"] = payload
return DeliveryResponse(
ok=True, status_code=200, data={"ok": True, "result": {"message_id": 42}}
)
monkeypatch.setattr("integrations.telegram.delivery.post_json", _stub_post_json)
ok, err, mid = post_telegram_message("chat-1", "hello", "secret-bot-tok")
assert ok is True
assert err == ""
assert mid == "42"
assert "/bot" in captured["url"]
assert captured["payload"]["chat_id"] == "chat-1"
assert captured["payload"]["text"] == "hello"
def test_transport_failure_redacts_token_in_error(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""When the shared helper returns ``ok=False`` because the transport
raised, telegram must still scrub the bot token out of the error
string before propagating it."""
from platform.notifications.delivery_transport import DeliveryResponse
bot_token = "1234567890:ABCDEFverysecretvalue"
leak_msg = f"connect failed for url=https://api.telegram.org/bot{bot_token}/sendMessage"
monkeypatch.setattr(
"integrations.telegram.delivery.post_json",
lambda *_a, **_kw: DeliveryResponse(ok=False, error=leak_msg),
)
ok, err, _ = post_telegram_message("chat-1", "hi", bot_token)
assert ok is False
assert bot_token not in err
assert "<redacted>" in err