mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-06 17:38:35 +00:00
658 lines
20 KiB
Python
658 lines
20 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from contextlib import suppress
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, MagicMock, call
|
|
|
|
import pytest
|
|
|
|
from nanobot.bus.events import OutboundMessage
|
|
from nanobot.channels import whatsapp as whatsapp_module
|
|
from nanobot.channels.whatsapp import (
|
|
WhatsAppChannel,
|
|
_legacy_bridge_config_fields,
|
|
_NeonizeAPI,
|
|
_ReactionTarget,
|
|
)
|
|
|
|
|
|
class _Proto:
|
|
def __init__(self, **kwargs):
|
|
self.__dict__.update(kwargs)
|
|
|
|
def HasField(self, name: str) -> bool: # noqa: N802 - protobuf compatibility
|
|
return _is_set(getattr(self, name, None))
|
|
|
|
def ListFields(self): # noqa: N802 - protobuf compatibility
|
|
return [
|
|
(SimpleNamespace(name=name), value)
|
|
for name, value in self.__dict__.items()
|
|
if _is_set(value)
|
|
]
|
|
|
|
|
|
def _is_set(value) -> bool:
|
|
if value is None:
|
|
return False
|
|
if isinstance(value, (str, bytes, list, tuple, dict, set)):
|
|
return bool(value)
|
|
return True
|
|
|
|
|
|
def _jid(user: str, server: str) -> _Proto:
|
|
return _Proto(User=user, Server=server, IsEmpty=False)
|
|
|
|
|
|
def _event(
|
|
*,
|
|
message: _Proto,
|
|
message_id: str = "m1",
|
|
chat: _Proto | None = None,
|
|
sender: _Proto | None = None,
|
|
sender_alt: _Proto | None = None,
|
|
is_group: bool = False,
|
|
timestamp: int = 1,
|
|
is_from_me: bool = False,
|
|
) -> _Proto:
|
|
source = _Proto(
|
|
Chat=chat or _jid("15551234567", "s.whatsapp.net"),
|
|
Sender=sender,
|
|
SenderAlt=sender_alt,
|
|
IsGroup=is_group,
|
|
IsFromMe=is_from_me,
|
|
)
|
|
return _Proto(
|
|
Info=_Proto(ID=message_id, Timestamp=timestamp, MessageSource=source),
|
|
Message=message,
|
|
)
|
|
|
|
|
|
def _make_channel(config: dict | None = None) -> WhatsAppChannel:
|
|
merged = {"enabled": True, "allowFrom": ["*"]}
|
|
if config:
|
|
merged.update(config)
|
|
ch = WhatsAppChannel(merged, MagicMock())
|
|
ch._started_at = 0
|
|
return ch
|
|
|
|
|
|
def _patch_neonize_api(monkeypatch) -> None:
|
|
chat_presence = SimpleNamespace(
|
|
CHAT_PRESENCE_COMPOSING="composing",
|
|
CHAT_PRESENCE_PAUSED="paused",
|
|
)
|
|
chat_presence_media = SimpleNamespace(CHAT_PRESENCE_MEDIA_TEXT="text")
|
|
monkeypatch.setattr(
|
|
whatsapp_module,
|
|
"_NEONIZE_API",
|
|
_NeonizeAPI(
|
|
NewAClient=object,
|
|
ConnectedEv=object(),
|
|
DisconnectedEv=object(),
|
|
MessageEv=object(),
|
|
PairStatusEv=object(),
|
|
build_jid=lambda user, server="s.whatsapp.net": (user, server),
|
|
ChatPresence=chat_presence,
|
|
ChatPresenceMedia=chat_presence_media,
|
|
),
|
|
)
|
|
|
|
|
|
class _FakeLoginClient:
|
|
def __init__(self) -> None:
|
|
self.handlers = {}
|
|
self.me = _Proto(JID=_jid("bot", "s.whatsapp.net"), LID=_jid("BOTLID", "lid"))
|
|
self.stop = AsyncMock()
|
|
|
|
def event(self, event_type):
|
|
def register(func):
|
|
self.handlers[event_type] = func
|
|
return func
|
|
|
|
return register
|
|
|
|
def qr(self, func):
|
|
self.qr_handler = func
|
|
return func
|
|
|
|
async def connect(self) -> None:
|
|
await self.handlers[whatsapp_module._NEONIZE_API.ConnectedEv](self, _Proto())
|
|
|
|
|
|
class _FailingConnectLoginClient(_FakeLoginClient):
|
|
async def connect(self) -> asyncio.Task[None]:
|
|
async def fail() -> None:
|
|
raise RuntimeError("dial failed")
|
|
|
|
return asyncio.create_task(fail())
|
|
|
|
|
|
def test_default_config_has_no_bridge_fields() -> None:
|
|
config = WhatsAppChannel.default_config()
|
|
|
|
assert "bridgeUrl" not in config
|
|
assert "bridgeToken" not in config
|
|
assert config["databasePath"] == ""
|
|
|
|
|
|
def test_legacy_bridge_config_fields_are_detected() -> None:
|
|
assert _legacy_bridge_config_fields({"bridgeUrl": "ws://localhost:3001"}) == ["bridgeUrl"]
|
|
assert _legacy_bridge_config_fields({"bridgeToken": "secret"}) == ["bridgeToken"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_succeeds_when_connected(monkeypatch) -> None:
|
|
_patch_neonize_api(monkeypatch)
|
|
client = _FakeLoginClient()
|
|
ch = _make_channel()
|
|
ch._new_client = MagicMock(return_value=client)
|
|
|
|
assert await ch.login() is True
|
|
assert ch._self_jids == {"bot@s.whatsapp.net", "bot", "BOTLID@lid", "BOTLID"}
|
|
client.stop.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_login_fails_when_connect_task_fails(monkeypatch) -> None:
|
|
_patch_neonize_api(monkeypatch)
|
|
client = _FailingConnectLoginClient()
|
|
ch = _make_channel()
|
|
ch._new_client = MagicMock(return_value=client)
|
|
|
|
assert await ch.login() is False
|
|
client.stop.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_text_uses_neonize_send_message(monkeypatch) -> None:
|
|
_patch_neonize_api(monkeypatch)
|
|
client = SimpleNamespace(
|
|
send_message=AsyncMock(),
|
|
send_image=AsyncMock(),
|
|
send_video=AsyncMock(),
|
|
send_audio=AsyncMock(),
|
|
send_document=AsyncMock(),
|
|
)
|
|
ch = _make_channel()
|
|
ch._client = client
|
|
ch._connected = True
|
|
|
|
await ch.send(OutboundMessage(channel="whatsapp", chat_id="12345@s.whatsapp.net", content="hi"))
|
|
|
|
client.send_message.assert_awaited_once_with(("12345", "s.whatsapp.net"), "hi")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_text_passes_metadata_mentions_to_neonize(monkeypatch) -> None:
|
|
_patch_neonize_api(monkeypatch)
|
|
client = SimpleNamespace(
|
|
send_message=AsyncMock(),
|
|
send_image=AsyncMock(),
|
|
send_video=AsyncMock(),
|
|
send_audio=AsyncMock(),
|
|
send_document=AsyncMock(),
|
|
)
|
|
ch = _make_channel()
|
|
ch._client = client
|
|
ch._connected = True
|
|
|
|
await ch.send(
|
|
OutboundMessage(
|
|
channel="whatsapp",
|
|
chat_id="12345@s.whatsapp.net",
|
|
content="hi",
|
|
metadata={
|
|
"mentions": [
|
|
"+15551234567@s.whatsapp.net",
|
|
{"jid": "15557654321@s.whatsapp.net"},
|
|
"not-a-number",
|
|
]
|
|
},
|
|
)
|
|
)
|
|
|
|
client.send_message.assert_awaited_once_with(
|
|
("12345", "s.whatsapp.net"),
|
|
"hi",
|
|
ghost_mentions="@15551234567 @15557654321",
|
|
mentions_are_lids=False,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_text_passes_lid_mentions_to_neonize(monkeypatch) -> None:
|
|
_patch_neonize_api(monkeypatch)
|
|
client = SimpleNamespace(send_message=AsyncMock())
|
|
ch = _make_channel()
|
|
ch._client = client
|
|
ch._connected = True
|
|
|
|
await ch.send(
|
|
OutboundMessage(
|
|
channel="whatsapp",
|
|
chat_id="12345@s.whatsapp.net",
|
|
content="hi",
|
|
metadata={"mentioned_jids": ["123456789012345@lid"]},
|
|
)
|
|
)
|
|
|
|
client.send_message.assert_awaited_once_with(
|
|
("12345", "s.whatsapp.net"),
|
|
"hi",
|
|
ghost_mentions="@123456789012345",
|
|
mentions_are_lids=True,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_inbound_message_starts_typing_and_reaction(monkeypatch) -> None:
|
|
_patch_neonize_api(monkeypatch)
|
|
client = SimpleNamespace(
|
|
download_any=AsyncMock(),
|
|
send_chat_presence=AsyncMock(),
|
|
build_reaction=AsyncMock(return_value="reaction-message"),
|
|
send_message=AsyncMock(),
|
|
)
|
|
ch = _make_channel({"reactEmoji": "👀"})
|
|
ch._client = client
|
|
ch._connected = True
|
|
ch._handle_message = AsyncMock()
|
|
|
|
await ch._handle_neonize_message(
|
|
client,
|
|
_event(
|
|
message=_Proto(conversation="hello"),
|
|
message_id="wamid.1",
|
|
chat=_jid("120363000", "g.us"),
|
|
sender=_jid("LID99", "lid"),
|
|
sender_alt=_jid("15559998888", "s.whatsapp.net"),
|
|
is_group=True,
|
|
),
|
|
)
|
|
await asyncio.sleep(0)
|
|
|
|
client.send_chat_presence.assert_any_await(
|
|
("120363000", "g.us"),
|
|
"composing",
|
|
"text",
|
|
)
|
|
client.build_reaction.assert_awaited_once_with(
|
|
("120363000", "g.us"),
|
|
("15559998888", "s.whatsapp.net"),
|
|
"wamid.1",
|
|
"👀",
|
|
)
|
|
assert call(("120363000", "g.us"), "reaction-message") in client.send_message.await_args_list
|
|
assert ch._reaction_targets["120363000@g.us"] == _ReactionTarget(
|
|
"wamid.1",
|
|
"15559998888@s.whatsapp.net",
|
|
)
|
|
|
|
ch._stop_typing("120363000@g.us")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_final_send_stops_typing_and_removes_reaction(monkeypatch) -> None:
|
|
_patch_neonize_api(monkeypatch)
|
|
client = SimpleNamespace(
|
|
send_message=AsyncMock(),
|
|
send_chat_presence=AsyncMock(),
|
|
build_reaction=AsyncMock(return_value="remove-reaction"),
|
|
)
|
|
ch = _make_channel()
|
|
ch._client = client
|
|
ch._connected = True
|
|
chat_id = "12345@s.whatsapp.net"
|
|
typing_task = asyncio.create_task(asyncio.sleep(60))
|
|
ch._typing_tasks[chat_id] = typing_task
|
|
ch._reaction_targets[chat_id] = _ReactionTarget("wamid.1", "15551234567@s.whatsapp.net")
|
|
|
|
await ch.send(OutboundMessage(channel="whatsapp", chat_id=chat_id, content="done"))
|
|
await asyncio.sleep(0)
|
|
|
|
assert typing_task.cancelled()
|
|
assert chat_id not in ch._typing_tasks
|
|
assert chat_id not in ch._reaction_targets
|
|
client.send_chat_presence.assert_awaited_once_with(
|
|
("12345", "s.whatsapp.net"),
|
|
"paused",
|
|
"text",
|
|
)
|
|
client.build_reaction.assert_awaited_once_with(
|
|
("12345", "s.whatsapp.net"),
|
|
("15551234567", "s.whatsapp.net"),
|
|
"wamid.1",
|
|
"",
|
|
)
|
|
client.send_message.assert_has_awaits(
|
|
[
|
|
call(("12345", "s.whatsapp.net"), "remove-reaction"),
|
|
call(("12345", "s.whatsapp.net"), "done"),
|
|
]
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_progress_send_keeps_typing_and_reaction(monkeypatch) -> None:
|
|
_patch_neonize_api(monkeypatch)
|
|
client = SimpleNamespace(
|
|
send_message=AsyncMock(),
|
|
send_chat_presence=AsyncMock(),
|
|
build_reaction=AsyncMock(return_value="remove-reaction"),
|
|
)
|
|
ch = _make_channel()
|
|
ch._client = client
|
|
ch._connected = True
|
|
chat_id = "12345@s.whatsapp.net"
|
|
typing_task = asyncio.create_task(asyncio.sleep(60))
|
|
ch._typing_tasks[chat_id] = typing_task
|
|
ch._reaction_targets[chat_id] = _ReactionTarget("wamid.1", "15551234567@s.whatsapp.net")
|
|
|
|
await ch.send(
|
|
OutboundMessage(
|
|
channel="whatsapp",
|
|
chat_id=chat_id,
|
|
content="working",
|
|
metadata={"_progress": True},
|
|
)
|
|
)
|
|
|
|
assert ch._typing_tasks[chat_id] is typing_task
|
|
assert ch._reaction_targets[chat_id] == _ReactionTarget(
|
|
"wamid.1",
|
|
"15551234567@s.whatsapp.net",
|
|
)
|
|
client.send_chat_presence.assert_not_awaited()
|
|
client.build_reaction.assert_not_awaited()
|
|
client.send_message.assert_awaited_once_with(("12345", "s.whatsapp.net"), "working")
|
|
|
|
typing_task.cancel()
|
|
with suppress(asyncio.CancelledError):
|
|
await typing_task
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_dispatches_by_mimetype(monkeypatch) -> None:
|
|
_patch_neonize_api(monkeypatch)
|
|
client = SimpleNamespace(
|
|
send_message=AsyncMock(),
|
|
send_image=AsyncMock(),
|
|
send_video=AsyncMock(),
|
|
send_audio=AsyncMock(),
|
|
send_document=AsyncMock(),
|
|
)
|
|
ch = _make_channel()
|
|
ch._client = client
|
|
ch._connected = True
|
|
|
|
await ch.send(
|
|
OutboundMessage(
|
|
channel="whatsapp",
|
|
chat_id="12345@s.whatsapp.net",
|
|
content="",
|
|
media=["photo.jpg", "clip.mp4", "voice.ogg", "report.pdf"],
|
|
)
|
|
)
|
|
|
|
jid = ("12345", "s.whatsapp.net")
|
|
client.send_image.assert_awaited_once_with(jid, "photo.jpg")
|
|
client.send_video.assert_awaited_once_with(jid, "clip.mp4")
|
|
client.send_audio.assert_awaited_once_with(jid, "voice.ogg")
|
|
client.send_document.assert_awaited_once_with(
|
|
jid,
|
|
"report.pdf",
|
|
filename="report.pdf",
|
|
mimetype="application/pdf",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_when_disconnected_raises() -> None:
|
|
ch = _make_channel()
|
|
|
|
with pytest.raises(RuntimeError, match="not connected"):
|
|
await ch.send(OutboundMessage(channel="whatsapp", chat_id="123", content="hi"))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_group_policy_mention_skips_unmentioned_group_message() -> None:
|
|
ch = _make_channel({"groupPolicy": "mention"})
|
|
ch._self_jids = {"bot@s.whatsapp.net", "bot"}
|
|
ch._handle_message = AsyncMock()
|
|
|
|
await ch._handle_neonize_message(
|
|
SimpleNamespace(download_any=AsyncMock()),
|
|
_event(
|
|
message=_Proto(conversation="hello group"),
|
|
chat=_jid("120363000", "g.us"),
|
|
sender=_jid("SENDERLID", "lid"),
|
|
is_group=True,
|
|
),
|
|
)
|
|
|
|
ch._handle_message.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_group_policy_mention_accepts_mention_and_prefers_phone_sender() -> None:
|
|
ch = _make_channel({"groupPolicy": "mention"})
|
|
ch._self_jids = {"bot@s.whatsapp.net", "bot"}
|
|
ch._handle_message = AsyncMock()
|
|
context = _Proto(mentionedJID=["bot@s.whatsapp.net"])
|
|
message = _Proto(extendedTextMessage=_Proto(text="hello @bot", contextInfo=context))
|
|
|
|
await ch._handle_neonize_message(
|
|
SimpleNamespace(download_any=AsyncMock()),
|
|
_event(
|
|
message=message,
|
|
chat=_jid("120363000", "g.us"),
|
|
sender=_jid("LID99", "lid"),
|
|
sender_alt=_jid("15559998888", "s.whatsapp.net"),
|
|
is_group=True,
|
|
),
|
|
)
|
|
|
|
kwargs = ch._handle_message.await_args.kwargs
|
|
assert kwargs["sender_id"] == "15559998888"
|
|
assert kwargs["chat_id"] == "120363000@g.us"
|
|
assert kwargs["metadata"]["lid"] == "LID99"
|
|
assert kwargs["metadata"]["phone"] == "15559998888"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_group_policy_mention_accepts_reply_to_bot() -> None:
|
|
ch = _make_channel({"groupPolicy": "mention"})
|
|
ch._self_jids = {"bot@s.whatsapp.net", "bot"}
|
|
ch._handle_message = AsyncMock()
|
|
context = _Proto(participant="bot@s.whatsapp.net")
|
|
message = _Proto(extendedTextMessage=_Proto(text="reply", contextInfo=context))
|
|
|
|
await ch._handle_neonize_message(
|
|
SimpleNamespace(download_any=AsyncMock()),
|
|
_event(
|
|
message=message,
|
|
chat=_jid("120363000", "g.us"),
|
|
sender=_jid("SENDERLID", "lid"),
|
|
is_group=True,
|
|
),
|
|
)
|
|
|
|
kwargs = ch._handle_message.await_args.kwargs
|
|
assert kwargs["metadata"]["is_reply_to_bot"] is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_group_sender_id_uses_participant_not_group_jid() -> None:
|
|
ch = WhatsAppChannel({"enabled": True, "allowFrom": ["SENDERLID"]}, MagicMock())
|
|
ch._started_at = 0
|
|
ch._handle_message = AsyncMock()
|
|
|
|
await ch._handle_neonize_message(
|
|
SimpleNamespace(download_any=AsyncMock()),
|
|
_event(
|
|
message=_Proto(conversation="hi"),
|
|
chat=_jid("120363000", "g.us"),
|
|
sender=_jid("SENDERLID", "lid"),
|
|
is_group=True,
|
|
),
|
|
)
|
|
|
|
kwargs = ch._handle_message.await_args.kwargs
|
|
assert kwargs["sender_id"] == "SENDERLID"
|
|
assert kwargs["metadata"]["participant"] == "SENDERLID@lid"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lid_to_phone_cache_resolves_lid_only_messages() -> None:
|
|
ch = _make_channel()
|
|
ch._handle_message = AsyncMock()
|
|
|
|
await ch._handle_neonize_message(
|
|
SimpleNamespace(download_any=AsyncMock()),
|
|
_event(
|
|
message=_Proto(conversation="first"),
|
|
message_id="c1",
|
|
chat=_jid("LID99", "lid"),
|
|
sender=_jid("LID99", "lid"),
|
|
sender_alt=_jid("5559999", "s.whatsapp.net"),
|
|
),
|
|
)
|
|
await ch._handle_neonize_message(
|
|
SimpleNamespace(download_any=AsyncMock()),
|
|
_event(
|
|
message=_Proto(conversation="second"),
|
|
message_id="c2",
|
|
chat=_jid("LID99", "lid"),
|
|
sender=_jid("LID99", "lid"),
|
|
),
|
|
)
|
|
|
|
assert ch._handle_message.await_args_list[1].kwargs["sender_id"] == "5559999"
|
|
|
|
|
|
def test_lid_mappings_from_config() -> None:
|
|
ch = WhatsAppChannel(
|
|
{"enabled": True, "lidMappings": {"123456789012345": "15551234567"}},
|
|
MagicMock(),
|
|
)
|
|
|
|
assert ch._lid_to_phone == {"123456789012345": "15551234567"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_image_media_is_downloaded_and_forwarded(monkeypatch, tmp_path) -> None:
|
|
monkeypatch.setattr(whatsapp_module, "get_media_dir", lambda channel: tmp_path / channel)
|
|
ch = _make_channel()
|
|
ch._handle_message = AsyncMock()
|
|
client = SimpleNamespace(download_any=AsyncMock())
|
|
message = _Proto(
|
|
imageMessage=_Proto(
|
|
caption="look",
|
|
mimetype="image/jpeg",
|
|
)
|
|
)
|
|
|
|
await ch._handle_neonize_message(
|
|
client,
|
|
_event(message=message, sender_alt=_jid("15551234567", "s.whatsapp.net")),
|
|
)
|
|
|
|
client.download_any.assert_awaited_once()
|
|
kwargs = ch._handle_message.await_args.kwargs
|
|
assert kwargs["content"].startswith("look\n[image: ")
|
|
assert len(kwargs["media"]) == 1
|
|
assert kwargs["media"][0].endswith(".jpg")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_voice_message_transcribes_and_drops_media_when_successful(
|
|
monkeypatch, tmp_path
|
|
) -> None:
|
|
monkeypatch.setattr(whatsapp_module, "get_media_dir", lambda channel: tmp_path / channel)
|
|
ch = _make_channel()
|
|
ch._handle_message = AsyncMock()
|
|
ch.transcribe_audio = AsyncMock(return_value="Hello from audio")
|
|
client = SimpleNamespace(download_any=AsyncMock())
|
|
message = _Proto(audioMessage=_Proto(mimetype="audio/ogg", PTT=True))
|
|
|
|
await ch._handle_neonize_message(
|
|
client,
|
|
_event(message=message, sender_alt=_jid("15551234567", "s.whatsapp.net")),
|
|
)
|
|
|
|
ch.transcribe_audio.assert_awaited_once()
|
|
kwargs = ch._handle_message.await_args.kwargs
|
|
assert kwargs["content"] == "Hello from audio"
|
|
assert kwargs["media"] == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unauthorized_voice_message_does_not_download_or_transcribe(
|
|
monkeypatch, tmp_path
|
|
) -> None:
|
|
monkeypatch.setattr(whatsapp_module, "get_media_dir", lambda channel: tmp_path / channel)
|
|
ch = WhatsAppChannel({"enabled": True, "allowFrom": ["allowed"]}, MagicMock())
|
|
ch._started_at = 0
|
|
ch._handle_message = AsyncMock()
|
|
ch.transcribe_audio = AsyncMock(return_value="blocked audio")
|
|
client = SimpleNamespace(download_any=AsyncMock())
|
|
|
|
await ch._handle_neonize_message(
|
|
client,
|
|
_event(
|
|
message=_Proto(audioMessage=_Proto(mimetype="audio/ogg", PTT=True)),
|
|
chat=_jid("blocked", "s.whatsapp.net"),
|
|
sender=_jid("blocked", "s.whatsapp.net"),
|
|
),
|
|
)
|
|
|
|
client.download_any.assert_not_awaited()
|
|
ch.transcribe_audio.assert_not_awaited()
|
|
ch._handle_message.assert_awaited_once()
|
|
kwargs = ch._handle_message.await_args.kwargs
|
|
assert kwargs["sender_id"] == "blocked"
|
|
assert kwargs["content"] == ""
|
|
assert kwargs["media"] == []
|
|
assert kwargs["is_dm"] is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unauthorized_dm_uses_base_pairing_flow(monkeypatch) -> None:
|
|
_patch_neonize_api(monkeypatch)
|
|
monkeypatch.setattr("nanobot.channels.base.generate_code", lambda _ch, _sid: "ABCD-EFGH")
|
|
monkeypatch.setattr("nanobot.channels.base.is_approved", lambda _ch, _sid: False)
|
|
client = SimpleNamespace(send_message=AsyncMock(), download_any=AsyncMock())
|
|
ch = WhatsAppChannel({"enabled": True, "allowFrom": []}, MagicMock())
|
|
ch._client = client
|
|
ch._connected = True
|
|
ch._started_at = 0
|
|
|
|
await ch._handle_neonize_message(
|
|
client,
|
|
_event(
|
|
message=_Proto(conversation="hello"),
|
|
chat=_jid("blocked", "s.whatsapp.net"),
|
|
sender=_jid("blocked", "s.whatsapp.net"),
|
|
),
|
|
)
|
|
|
|
client.download_any.assert_not_awaited()
|
|
client.send_message.assert_awaited_once()
|
|
assert client.send_message.await_args.args[0] == ("blocked", "s.whatsapp.net")
|
|
assert "ABCD-EFGH" in client.send_message.await_args.args[1]
|
|
|
|
|
|
def test_reset_database_removes_sqlite_sidecars(tmp_path) -> None:
|
|
db = tmp_path / "neonize.db"
|
|
wal = tmp_path / "neonize.db-wal"
|
|
shm = tmp_path / "neonize.db-shm"
|
|
for path in (db, wal, shm):
|
|
path.write_text("x", encoding="utf-8")
|
|
|
|
WhatsAppChannel._reset_database(db)
|
|
|
|
assert not db.exists()
|
|
assert not wal.exists()
|
|
assert not shm.exists()
|