from types import SimpleNamespace import pytest from nanobot.bus.events import OutboundMessage from nanobot.bus.queue import MessageBus from nanobot.channels.base import BaseChannel class _DummyChannel(BaseChannel): name = "dummy" _sent: list[OutboundMessage] def __init__(self, config, bus): super().__init__(config, bus) self._sent = [] async def start(self) -> None: return None async def stop(self) -> None: return None async def send(self, msg: OutboundMessage) -> None: self._sent.append(msg) def test_is_allowed_requires_exact_match() -> None: channel = _DummyChannel(SimpleNamespace(allow_from=["allow@email.com"]), MessageBus()) assert channel.is_allowed("allow@email.com") is True assert channel.is_allowed("attacker|allow@email.com") is False def test_is_allowed_supports_dict_allow_from_alias() -> None: channel = _DummyChannel({"allowFrom": ["alice"]}, MessageBus()) assert channel.is_allowed("alice") is True def test_is_allowed_denies_empty_dict_allow_from() -> None: channel = _DummyChannel({"allow_from": []}, MessageBus()) assert channel.is_allowed("alice") is False def test_is_allowed_handles_none_allow_from() -> None: channel = _DummyChannel({"allow_from": None}, MessageBus()) assert channel.is_allowed("alice") is False channel2 = _DummyChannel({"allowFrom": None}, MessageBus()) assert channel2.is_allowed("alice") is False def test_is_allowed_star_allows_all() -> None: channel = _DummyChannel({"allowFrom": ["*"]}, MessageBus()) assert channel.is_allowed("anyone") is True def test_is_allowed_pairing_fallback(monkeypatch) -> None: channel = _DummyChannel({"allowFrom": []}, MessageBus()) monkeypatch.setattr( "nanobot.channels.base.is_approved", lambda _ch, sid: sid == "paired" ) assert channel.is_allowed("paired") is True assert channel.is_allowed("unknown") is False @pytest.mark.asyncio async def test_handle_message_dm_sends_pairing_code(monkeypatch) -> None: channel = _DummyChannel({"allowFrom": []}, MessageBus()) monkeypatch.setattr( "nanobot.channels.base.generate_code", lambda _ch, sid: "ABCD-EFGH" ) await channel._handle_message( sender_id="stranger", chat_id="chat1", content="hello", is_dm=True ) assert len(channel._sent) == 1 msg = channel._sent[0] assert "ABCD-EFGH" in msg.content assert msg.metadata.get("_pairing_code") == "ABCD-EFGH" @pytest.mark.asyncio async def test_dm_during_transient_store_failure_keeps_approvals( tmp_path, monkeypatch ) -> None: """An unapproved DM while pairing.json is unreadable must not wipe approvals. The pairing store treated a transient OSError like corruption and returned an empty store; the DM pairing path then persisted that empty view, erasing every approved sender. """ import builtins from pathlib import Path from nanobot.pairing import store path = tmp_path / "pairing.json" monkeypatch.setattr(store, "_store_path", lambda: path) code = store.generate_code("dummy", "friend") store.approve_code(code) channel = _DummyChannel({"allowFrom": []}, MessageBus()) real_open = builtins.open def flaky_open(file, mode="r", *args, **kwargs): try: same = Path(file) == path except TypeError: same = False if same and "r" in mode and "+" not in mode: raise PermissionError(13, "temporarily locked", str(path)) return real_open(file, mode, *args, **kwargs) with monkeypatch.context() as m: m.setattr(builtins, "open", flaky_open) await channel._handle_message( sender_id="stranger", chat_id="chat1", content="hello", is_dm=True ) assert channel._sent == [] assert store.is_approved("dummy", "friend") is True @pytest.mark.asyncio async def test_handle_message_group_ignores_unknown() -> None: channel = _DummyChannel({"allowFrom": []}, MessageBus()) await channel._handle_message( sender_id="stranger", chat_id="chat1", content="hello", is_dm=False ) assert channel._sent == [] @pytest.mark.asyncio async def test_handle_message_uses_authorization_id_without_changing_sender() -> None: bus = MessageBus() channel = _DummyChannel({"allowFrom": ["group@g.us"]}, bus) await channel._handle_message( sender_id="member-lid", authorization_id="group@g.us", chat_id="group@g.us", content="hello", ) msg = await bus.consume_inbound() assert msg.sender_id == "member-lid" assert msg.chat_id == "group@g.us" @pytest.mark.asyncio async def test_handle_message_rejects_when_authorization_id_is_not_allowed() -> None: bus = MessageBus() channel = _DummyChannel({"allowFrom": ["member-lid"]}, bus) await channel._handle_message( sender_id="member-lid", authorization_id="other-group@g.us", chat_id="other-group@g.us", content="hello", ) assert bus.inbound_size == 0