Files
meshcore-bot/tests/test_contact_command.py
T
Gerard Hickeyandagessaman f9b1cbddb8 feat(command): Add contact command (#293)
* feat(command): Add contact command

The contact command allows the user to ask the bot to provide its
public key as a clickable contact. This allows the user to then DM the
bot outside of a channel.

Signed-off-by: Gerard Hickey <hickey@kinetic-compute.com>

* fix(contact): harden matching and self_info handling, add tests and docs

Review follow-ups on the contact command:

- Match through _cleaned_content_matches so a non-matching message keeps its
  original content. The raw cleanup_message_for_matching call rewrote
  message.content in place during the keyword scan, which is the #267
  regression every other command already avoids.
- Match against self.keywords so a configured alias works.
- Read self_info defensively (dict or object, may be absent) and validate the
  public key instead of sending a literal "<None:1:None>" over the air.
- Drop the hardcoded '!' prefix strip in execute; matching already normalizes
  the content, and prefixes are configurable.
- Forward skip_channel_check to the base can_execute.
- Remove copy-paste docstring leftovers from the roll command.
- Move [Contact_Command] into the alphabetical run in config.ini.example and
  fix the "chanels" typo.
- Add tests, a docs/command-reference.md entry, and flesh out the changelog.

---------

Signed-off-by: Gerard Hickey <hickey@kinetic-compute.com>
Co-authored-by: agessaman <adam@gessaman.com>
2026-09-19 20:19:38 -07:00

103 lines
3.9 KiB
Python

"""Tests for modules.commands.contact_command."""
import asyncio
import configparser
from unittest.mock import AsyncMock, MagicMock, Mock
from modules.commands.contact_command import ContactCommand
from tests.conftest import mock_message
PUBKEY = "f5d2b56d19b24412756933e917d4632e088cdd5daeadc9002feca73bf5d2b56d"
def _make_bot(self_info=None):
bot = MagicMock()
bot.logger = Mock()
config = configparser.ConfigParser()
config.add_section("Bot")
config.set("Bot", "bot_name", "TestBot")
config.set("Bot", "respond_to_mentions", "also")
config.add_section("Channels")
config.set("Channels", "monitor_channels", "general")
config.set("Channels", "respond_to_dms", "true")
config.add_section("Keywords")
config.add_section("Contact_Command")
config.set("Contact_Command", "enabled", "true")
bot.config = config
bot.translator = MagicMock()
bot.translator.translate = Mock(side_effect=lambda key, **kw: key)
bot.command_manager = MagicMock()
bot.command_manager.monitor_channels = ["general"]
bot.meshcore = MagicMock()
bot.meshcore.self_info = self_info
return bot
def _make_command(self_info=None):
cmd = ContactCommand(_make_bot(self_info))
cmd.send_response = AsyncMock(return_value=True)
return cmd
class TestMatching:
def test_matches_bare_keyword(self):
cmd = _make_command()
assert cmd.matches_keyword(mock_message("contact")) is True
def test_does_not_match_with_arguments(self):
cmd = _make_command()
assert cmd.matches_keyword(mock_message("contact me")) is False
def test_matches_configured_alias(self):
bot = _make_bot()
bot.config.set("Contact_Command", "aliases", "card")
cmd = ContactCommand(bot)
assert cmd.matches_keyword(mock_message("card")) is True
def test_non_match_leaves_message_content_untouched(self):
# Regression guard for #267: the keyword scan must not rewrite
# overheard traffic when this command does not match.
cmd = _make_command()
message = mock_message("@[TestBot] wx 98101")
assert cmd.matches_keyword(message) is False
assert message.content == "@[TestBot] wx 98101"
class TestExecute:
def test_sends_contact_card(self):
cmd = _make_command({"public_key": PUBKEY, "name": "TestBot"})
assert asyncio.run(cmd.execute(mock_message("contact"))) is True
cmd.send_response.assert_awaited_once()
assert cmd.send_response.await_args[0][1] == f"<{PUBKEY}:1:TestBot>"
def test_reads_self_info_object(self):
self_info = MagicMock(spec=["public_key", "name"])
self_info.public_key = PUBKEY.upper()
self_info.name = "TestBot"
cmd = _make_command(self_info)
assert asyncio.run(cmd.execute(mock_message("contact"))) is True
assert cmd.send_response.await_args[0][1] == f"<{PUBKEY}:1:TestBot>"
def test_missing_self_info_reports_unavailable(self):
cmd = _make_command(None)
assert asyncio.run(cmd.execute(mock_message("contact"))) is True
assert cmd.send_response.await_args[0][1] == "commands.contact.unavailable"
def test_malformed_public_key_reports_unavailable(self):
cmd = _make_command({"public_key": "nothex", "name": "TestBot"})
asyncio.run(cmd.execute(mock_message("contact")))
assert cmd.send_response.await_args[0][1] == "commands.contact.unavailable"
def test_missing_name_reports_unavailable(self):
cmd = _make_command({"public_key": PUBKEY})
asyncio.run(cmd.execute(mock_message("contact")))
assert cmd.send_response.await_args[0][1] == "commands.contact.unavailable"
class TestEnabledFlag:
def test_disabled_blocks_execution(self):
bot = _make_bot({"public_key": PUBKEY, "name": "TestBot"})
bot.config.set("Contact_Command", "enabled", "false")
cmd = ContactCommand(bot)
assert cmd.can_execute(mock_message("contact")) is False