mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-10-01 11:57:54 +00:00
* 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>
103 lines
3.9 KiB
Python
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
|