mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-03-30 12:05:38 +00:00
- Introduced `strip_optional_quotes` function to handle monitor channel values with optional surrounding quotes. - Updated `load_monitor_channels` method in `CommandManager` to utilize the new function, ensuring compatibility with quoted and unquoted channel configurations. - Modified `generate_samples` function to apply the same logic for consistency. - Added tests for `strip_optional_quotes` to validate behavior with various input cases.
281 lines
11 KiB
Python
281 lines
11 KiB
Python
"""Tests for modules.command_manager."""
|
|
|
|
import time
|
|
|
|
import pytest
|
|
from configparser import ConfigParser
|
|
from unittest.mock import Mock, MagicMock, patch, AsyncMock
|
|
|
|
from modules.command_manager import CommandManager, InternetStatusCache
|
|
from modules.models import MeshMessage
|
|
from tests.conftest import mock_message
|
|
|
|
|
|
@pytest.fixture
|
|
def cm_bot(mock_logger):
|
|
"""Mock bot for CommandManager tests."""
|
|
bot = Mock()
|
|
bot.logger = mock_logger
|
|
bot.config = ConfigParser()
|
|
bot.config.add_section("Bot")
|
|
bot.config.set("Bot", "bot_name", "TestBot")
|
|
bot.config.add_section("Channels")
|
|
bot.config.set("Channels", "monitor_channels", "general,test")
|
|
bot.config.set("Channels", "respond_to_dms", "true")
|
|
bot.config.add_section("Keywords")
|
|
bot.config.set("Keywords", "ping", "Pong!")
|
|
bot.config.set("Keywords", "test", "ack")
|
|
bot.translator = Mock()
|
|
# Translator returns "key: kwarg_values" so assertions can check content
|
|
bot.translator.translate = Mock(
|
|
side_effect=lambda key, **kw: f"{key}: {' '.join(str(v) for v in kw.values())}"
|
|
)
|
|
bot.meshcore = None
|
|
bot.rate_limiter = Mock()
|
|
bot.rate_limiter.can_send = Mock(return_value=True)
|
|
bot.bot_tx_rate_limiter = Mock()
|
|
bot.bot_tx_rate_limiter.wait_for_tx = Mock()
|
|
bot.tx_delay_ms = 0
|
|
return bot
|
|
|
|
|
|
def make_manager(bot, commands=None):
|
|
"""Create CommandManager with mocked PluginLoader."""
|
|
with patch("modules.command_manager.PluginLoader") as mock_loader_class:
|
|
mock_loader = Mock()
|
|
mock_loader.load_all_plugins = Mock(return_value=commands or {})
|
|
mock_loader_class.return_value = mock_loader
|
|
return CommandManager(bot)
|
|
|
|
|
|
class TestLoadKeywords:
|
|
"""Tests for keyword loading from config."""
|
|
|
|
def test_load_keywords_from_config(self, cm_bot):
|
|
manager = make_manager(cm_bot)
|
|
assert manager.keywords["ping"] == "Pong!"
|
|
assert manager.keywords["test"] == "ack"
|
|
|
|
def test_load_keywords_strips_quotes(self, cm_bot):
|
|
cm_bot.config.set("Keywords", "quoted", '"Hello World"')
|
|
manager = make_manager(cm_bot)
|
|
assert manager.keywords["quoted"] == "Hello World"
|
|
|
|
def test_load_keywords_decodes_escapes(self, cm_bot):
|
|
cm_bot.config.set("Keywords", "multiline", r"Line1\nLine2")
|
|
manager = make_manager(cm_bot)
|
|
assert "\n" in manager.keywords["multiline"]
|
|
|
|
def test_load_keywords_empty_section(self, cm_bot):
|
|
cm_bot.config.remove_section("Keywords")
|
|
cm_bot.config.add_section("Keywords")
|
|
manager = make_manager(cm_bot)
|
|
assert manager.keywords == {}
|
|
|
|
|
|
class TestLoadBannedUsers:
|
|
"""Tests for banned users loading."""
|
|
|
|
def test_load_banned_users_from_config(self, cm_bot):
|
|
cm_bot.config.add_section("Banned_Users")
|
|
cm_bot.config.set("Banned_Users", "banned_users", "BadUser1, BadUser2")
|
|
manager = make_manager(cm_bot)
|
|
assert "BadUser1" in manager.banned_users
|
|
assert "BadUser2" in manager.banned_users
|
|
|
|
def test_load_banned_users_empty(self, cm_bot):
|
|
manager = make_manager(cm_bot)
|
|
assert manager.banned_users == []
|
|
|
|
def test_load_banned_users_whitespace_handling(self, cm_bot):
|
|
cm_bot.config.add_section("Banned_Users")
|
|
cm_bot.config.set("Banned_Users", "banned_users", " user1 , user2 ")
|
|
manager = make_manager(cm_bot)
|
|
assert "user1" in manager.banned_users
|
|
assert "user2" in manager.banned_users
|
|
|
|
|
|
class TestIsUserBanned:
|
|
"""Tests for ban checking logic."""
|
|
|
|
def test_exact_match(self, cm_bot):
|
|
cm_bot.config.add_section("Banned_Users")
|
|
cm_bot.config.set("Banned_Users", "banned_users", "BadUser")
|
|
manager = make_manager(cm_bot)
|
|
assert manager.is_user_banned("BadUser") is True
|
|
|
|
def test_prefix_match(self, cm_bot):
|
|
cm_bot.config.add_section("Banned_Users")
|
|
cm_bot.config.set("Banned_Users", "banned_users", "BadUser")
|
|
manager = make_manager(cm_bot)
|
|
assert manager.is_user_banned("BadUser 123") is True
|
|
|
|
def test_no_match(self, cm_bot):
|
|
cm_bot.config.add_section("Banned_Users")
|
|
cm_bot.config.set("Banned_Users", "banned_users", "BadUser")
|
|
manager = make_manager(cm_bot)
|
|
assert manager.is_user_banned("GoodUser") is False
|
|
|
|
def test_none_sender(self, cm_bot):
|
|
manager = make_manager(cm_bot)
|
|
assert manager.is_user_banned(None) is False
|
|
|
|
|
|
class TestChannelTriggerAllowed:
|
|
"""Tests for _is_channel_trigger_allowed."""
|
|
|
|
def test_dm_always_allowed(self, cm_bot):
|
|
cm_bot.config.set("Channels", "channel_keywords", "ping")
|
|
manager = make_manager(cm_bot)
|
|
msg = mock_message(content="wx", is_dm=True)
|
|
assert manager._is_channel_trigger_allowed("wx", msg) is True
|
|
|
|
def test_none_whitelist_allows_all(self, cm_bot):
|
|
manager = make_manager(cm_bot)
|
|
assert manager.channel_keywords is None
|
|
msg = mock_message(content="anything", channel="general", is_dm=False)
|
|
assert manager._is_channel_trigger_allowed("anything", msg) is True
|
|
|
|
def test_whitelist_allows_listed(self, cm_bot):
|
|
cm_bot.config.set("Channels", "channel_keywords", "ping, help")
|
|
manager = make_manager(cm_bot)
|
|
msg = mock_message(content="ping", channel="general", is_dm=False)
|
|
assert manager._is_channel_trigger_allowed("ping", msg) is True
|
|
|
|
def test_whitelist_blocks_unlisted(self, cm_bot):
|
|
cm_bot.config.set("Channels", "channel_keywords", "ping, help")
|
|
manager = make_manager(cm_bot)
|
|
msg = mock_message(content="wx", channel="general", is_dm=False)
|
|
assert manager._is_channel_trigger_allowed("wx", msg) is False
|
|
|
|
|
|
class TestLoadMonitorChannels:
|
|
"""Tests for monitor channels loading."""
|
|
|
|
def test_load_monitor_channels(self, cm_bot):
|
|
manager = make_manager(cm_bot)
|
|
assert "general" in manager.monitor_channels
|
|
assert "test" in manager.monitor_channels
|
|
assert len(manager.monitor_channels) == 2
|
|
|
|
def test_load_monitor_channels_empty(self, cm_bot):
|
|
cm_bot.config.set("Channels", "monitor_channels", "")
|
|
manager = make_manager(cm_bot)
|
|
assert manager.monitor_channels == []
|
|
|
|
def test_load_monitor_channels_quoted(self, cm_bot):
|
|
"""Quoted monitor_channels (e.g. \"#bot,#bot-everett,#bots\") is supported."""
|
|
cm_bot.config.set("Channels", "monitor_channels", '"#bot,#bot-everett,#bots"')
|
|
manager = make_manager(cm_bot)
|
|
assert manager.monitor_channels == ["#bot", "#bot-everett", "#bots"]
|
|
|
|
|
|
class TestLoadChannelKeywords:
|
|
"""Tests for channel keyword whitelist loading."""
|
|
|
|
def test_load_channel_keywords_returns_list(self, cm_bot):
|
|
cm_bot.config.set("Channels", "channel_keywords", "ping, wx, help")
|
|
manager = make_manager(cm_bot)
|
|
assert isinstance(manager.channel_keywords, list)
|
|
assert "ping" in manager.channel_keywords
|
|
assert "wx" in manager.channel_keywords
|
|
assert "help" in manager.channel_keywords
|
|
|
|
def test_load_channel_keywords_empty_returns_none(self, cm_bot):
|
|
cm_bot.config.set("Channels", "channel_keywords", "")
|
|
manager = make_manager(cm_bot)
|
|
assert manager.channel_keywords is None
|
|
|
|
def test_load_channel_keywords_not_set_returns_none(self, cm_bot):
|
|
manager = make_manager(cm_bot)
|
|
assert manager.channel_keywords is None
|
|
|
|
|
|
class TestCheckKeywords:
|
|
"""Tests for check_keywords() message matching."""
|
|
|
|
def test_exact_keyword_match(self, cm_bot):
|
|
manager = make_manager(cm_bot)
|
|
msg = mock_message(content="ping", channel="general", is_dm=False)
|
|
matches = manager.check_keywords(msg)
|
|
assert any(trigger == "ping" for trigger, _ in matches)
|
|
|
|
def test_prefix_required_blocks_bare_keyword(self, cm_bot):
|
|
cm_bot.config.set("Bot", "command_prefix", "!")
|
|
manager = make_manager(cm_bot)
|
|
msg = mock_message(content="ping", channel="general", is_dm=False)
|
|
matches = manager.check_keywords(msg)
|
|
assert len(matches) == 0
|
|
|
|
def test_prefix_matches(self, cm_bot):
|
|
cm_bot.config.set("Bot", "command_prefix", "!")
|
|
manager = make_manager(cm_bot)
|
|
msg = mock_message(content="!ping", channel="general", is_dm=False)
|
|
matches = manager.check_keywords(msg)
|
|
assert any(trigger == "ping" for trigger, _ in matches)
|
|
|
|
def test_wrong_channel_no_match(self, cm_bot):
|
|
manager = make_manager(cm_bot)
|
|
msg = mock_message(content="ping", channel="other", is_dm=False)
|
|
matches = manager.check_keywords(msg)
|
|
assert len(matches) == 0
|
|
|
|
def test_dm_allowed(self, cm_bot):
|
|
manager = make_manager(cm_bot)
|
|
msg = mock_message(content="ping", is_dm=True)
|
|
matches = manager.check_keywords(msg)
|
|
assert any(trigger == "ping" for trigger, _ in matches)
|
|
|
|
def test_help_routing(self, cm_bot):
|
|
manager = make_manager(cm_bot)
|
|
msg = mock_message(content="help", is_dm=True)
|
|
matches = manager.check_keywords(msg)
|
|
assert any(trigger == "help" for trigger, _ in matches)
|
|
|
|
|
|
class TestGetHelpForCommand:
|
|
"""Tests for command-specific help."""
|
|
|
|
def test_known_command_returns_help(self, cm_bot):
|
|
mock_cmd = MagicMock()
|
|
mock_cmd.keywords = ["wx"]
|
|
mock_cmd.get_help_text = Mock(return_value="Weather forecast info")
|
|
mock_cmd.dm_only = False
|
|
mock_cmd.requires_internet = False
|
|
manager = make_manager(cm_bot, commands={"wx": mock_cmd})
|
|
result = manager.get_help_for_command("wx")
|
|
# Translator receives help_text as kwarg, so it appears in the output
|
|
assert "Weather forecast info" in result
|
|
# Verify translator was called with the right key
|
|
cm_bot.translator.translate.assert_called_with(
|
|
"commands.help.specific", command="wx", help_text="Weather forecast info"
|
|
)
|
|
|
|
def test_unknown_command_returns_error(self, cm_bot):
|
|
manager = make_manager(cm_bot)
|
|
result = manager.get_help_for_command("nonexistent")
|
|
# Translator receives 'commands.help.unknown' key with command name
|
|
cm_bot.translator.translate.assert_called()
|
|
call_args = cm_bot.translator.translate.call_args
|
|
assert call_args[0][0] == "commands.help.unknown"
|
|
assert call_args[1]["command"] == "nonexistent"
|
|
|
|
|
|
class TestInternetStatusCache:
|
|
"""Tests for InternetStatusCache."""
|
|
|
|
def test_is_valid_fresh(self):
|
|
cache = InternetStatusCache(has_internet=True, timestamp=time.time())
|
|
assert cache.is_valid(30) is True
|
|
|
|
def test_is_valid_stale(self):
|
|
cache = InternetStatusCache(has_internet=True, timestamp=time.time() - 60)
|
|
assert cache.is_valid(30) is False
|
|
|
|
def test_get_lock_lazy_creation(self):
|
|
cache = InternetStatusCache(has_internet=True, timestamp=0)
|
|
assert cache._lock is None
|
|
lock1 = cache._get_lock()
|
|
lock2 = cache._get_lock()
|
|
assert lock1 is lock2
|