test: cover WhatsApp read receipts

maintainer edit: add focused coverage for the new best-effort mark_read path and remove an unused helper argument.
This commit is contained in:
chengyongru
2026-06-30 15:21:25 +08:00
committed by Xubin Ren
parent 839d1ecfb1
commit 4c0e9b9f46
2 changed files with 75 additions and 4 deletions
+2 -4
View File
@@ -499,9 +499,7 @@ class WhatsAppChannel(BaseChannel):
self._self_jids.add(jid)
self._self_jids.add(_bare_jid(jid))
async def _send_read_receipt(
self, client: Any, info: Any, source: Any, message_id: str
) -> None:
async def _send_read_receipt(self, client: Any, source: Any, message_id: str) -> None:
"""Send a read receipt (blue double-check) for an incoming message.
Best-effort: any failure is logged at debug level and swallowed so it
@@ -559,7 +557,7 @@ class WhatsAppChannel(BaseChannel):
self._processed_message_ids.popitem(last=False)
# Mark the incoming message as read (blue double-check). Best-effort.
await self._send_read_receipt(client, info, source, message_id)
await self._send_read_receipt(client, source, message_id)
participant_jid = _normalize_jid(_safe_attr(source, "Sender"))
sender_alt_jid = _normalize_jid(_safe_attr(source, "SenderAlt"))
+73
View File
@@ -1,6 +1,8 @@
from __future__ import annotations
import asyncio
import sys
import types
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
@@ -86,6 +88,23 @@ def _patch_neonize_api(monkeypatch) -> None:
)
def _patch_receipt_type(monkeypatch):
neonize = types.ModuleType("neonize")
utils = types.ModuleType("neonize.utils")
enum = types.ModuleType("neonize.utils.enum")
class ReceiptType:
READ = "read"
enum.ReceiptType = ReceiptType
neonize.utils = utils
utils.enum = enum
monkeypatch.setitem(sys.modules, "neonize", neonize)
monkeypatch.setitem(sys.modules, "neonize.utils", utils)
monkeypatch.setitem(sys.modules, "neonize.utils.enum", enum)
return ReceiptType
class _FakeLoginClient:
def __init__(self) -> None:
self.handlers = {}
@@ -301,6 +320,60 @@ async def test_group_sender_id_uses_participant_not_group_jid() -> None:
assert kwargs["metadata"]["participant"] == "SENDERLID@lid"
@pytest.mark.asyncio
async def test_read_receipt_is_requested_once_after_dedup() -> None:
ch = _make_channel()
ch._send_read_receipt = AsyncMock()
ch._handle_message = AsyncMock()
client = SimpleNamespace(download_any=AsyncMock())
event = _event(
message=_Proto(conversation="hi"),
sender=_jid("15551234567", "s.whatsapp.net"),
)
await ch._handle_neonize_message(client, event)
await ch._handle_neonize_message(client, event)
ch._send_read_receipt.assert_awaited_once_with(
client,
event.Info.MessageSource,
"m1",
)
ch._handle_message.assert_awaited_once()
@pytest.mark.asyncio
async def test_send_read_receipt_uses_mark_read_and_swallows_failures(monkeypatch) -> None:
receipt_type = _patch_receipt_type(monkeypatch)
ch = _make_channel()
source = _event(
message=_Proto(conversation="hi"),
sender=_jid("15551234567", "s.whatsapp.net"),
).Info.MessageSource
client = SimpleNamespace(
mark_read=AsyncMock(),
download_any=AsyncMock(),
)
await ch._send_read_receipt(client, source, "m1")
client.mark_read.assert_awaited_once_with(
"m1",
chat=source.Chat,
sender=source.Sender,
receipt=receipt_type.READ,
)
failing_client = SimpleNamespace(
mark_read=AsyncMock(side_effect=RuntimeError("boom")),
download_any=AsyncMock(),
)
await ch._send_read_receipt(failing_client, source, "m2")
failing_client.mark_read.assert_awaited_once()
@pytest.mark.asyncio
async def test_lid_to_phone_cache_resolves_lid_only_messages() -> None:
ch = _make_channel()