feat: add Mumble server admin plugin (!mu)
Admin-only command with subcommand dispatch for server management: kick, ban, mute/unmute, deafen/undeafen, move, users, channels, mkchan, rmchan, rename, desc. Auto-loads on merlin via except_plugins.
This commit is contained in:
498
tests/test_mumble_admin.py
Normal file
498
tests/test_mumble_admin.py
Normal file
@@ -0,0 +1,498 @@
|
||||
"""Tests for the mumble_admin plugin."""
|
||||
|
||||
import asyncio
|
||||
import importlib.util
|
||||
from dataclasses import dataclass, field
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# -- Load plugin module directly ---------------------------------------------
|
||||
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"mumble_admin", "plugins/mumble_admin.py",
|
||||
)
|
||||
_mod = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(_mod)
|
||||
|
||||
cmd_mu = _mod.cmd_mu
|
||||
_find_user = _mod._find_user
|
||||
_find_channel = _mod._find_channel
|
||||
_channel_name = _mod._channel_name
|
||||
|
||||
|
||||
# -- Fakes -------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class _FakeMessage:
|
||||
text: str = ""
|
||||
nick: str = "admin"
|
||||
prefix: str = "admin"
|
||||
target: str = "0"
|
||||
is_channel: bool = True
|
||||
params: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class _FakeRegistry:
|
||||
_bots: dict = field(default_factory=dict)
|
||||
|
||||
def __init__(self):
|
||||
self._bots = {}
|
||||
|
||||
|
||||
class _FakeBot:
|
||||
def __init__(self, users=None, channels=None):
|
||||
self.registry = _FakeRegistry()
|
||||
self._mumble = MagicMock()
|
||||
if users is not None:
|
||||
self._mumble.users = users
|
||||
else:
|
||||
self._mumble.users = {}
|
||||
if channels is not None:
|
||||
self._mumble.channels = channels
|
||||
self._replies: list[str] = []
|
||||
|
||||
async def reply(self, message, text):
|
||||
self._replies.append(text)
|
||||
|
||||
async def send(self, target, text):
|
||||
self._replies.append(text)
|
||||
|
||||
|
||||
def _make_user(name, channel_id=0, mute=False, deaf=False,
|
||||
self_mute=False, self_deaf=False):
|
||||
"""Create a fake pymumble user (dict with methods)."""
|
||||
u = MagicMock()
|
||||
u.__getitem__ = lambda s, k: {
|
||||
"name": name,
|
||||
"channel_id": channel_id,
|
||||
"mute": mute,
|
||||
"deaf": deaf,
|
||||
"self_mute": self_mute,
|
||||
"self_deaf": self_deaf,
|
||||
}[k]
|
||||
u.get = lambda k, d=None: {
|
||||
"name": name,
|
||||
"channel_id": channel_id,
|
||||
"mute": mute,
|
||||
"deaf": deaf,
|
||||
"self_mute": self_mute,
|
||||
"self_deaf": self_deaf,
|
||||
}.get(k, d)
|
||||
return u
|
||||
|
||||
|
||||
def _make_channel(name, channel_id=0, parent=0):
|
||||
"""Create a fake pymumble channel (dict with methods)."""
|
||||
c = MagicMock()
|
||||
c.__getitem__ = lambda s, k: {
|
||||
"name": name,
|
||||
"channel_id": channel_id,
|
||||
"parent": parent,
|
||||
}[k]
|
||||
c.get = lambda k, d=None: {
|
||||
"name": name,
|
||||
"channel_id": channel_id,
|
||||
"parent": parent,
|
||||
}.get(k, d)
|
||||
return c
|
||||
|
||||
|
||||
# -- TestFindUser ------------------------------------------------------------
|
||||
|
||||
|
||||
class TestFindUser:
|
||||
def test_case_insensitive(self):
|
||||
alice = _make_user("Alice")
|
||||
bot = _FakeBot(users={1: alice})
|
||||
assert _find_user(bot, "alice") is alice
|
||||
assert _find_user(bot, "ALICE") is alice
|
||||
assert _find_user(bot, "Alice") is alice
|
||||
|
||||
def test_not_found(self):
|
||||
bot = _FakeBot(users={1: _make_user("Alice")})
|
||||
assert _find_user(bot, "Bob") is None
|
||||
|
||||
def test_no_mumble(self):
|
||||
bot = _FakeBot()
|
||||
bot._mumble = None
|
||||
assert _find_user(bot, "anyone") is None
|
||||
|
||||
|
||||
# -- TestFindChannel ---------------------------------------------------------
|
||||
|
||||
|
||||
class TestFindChannel:
|
||||
def test_case_insensitive(self):
|
||||
lobby = _make_channel("Lobby", channel_id=0)
|
||||
bot = _FakeBot(channels={0: lobby})
|
||||
assert _find_channel(bot, "lobby") is lobby
|
||||
assert _find_channel(bot, "LOBBY") is lobby
|
||||
|
||||
def test_not_found(self):
|
||||
bot = _FakeBot(channels={0: _make_channel("Lobby")})
|
||||
assert _find_channel(bot, "AFK") is None
|
||||
|
||||
def test_no_mumble(self):
|
||||
bot = _FakeBot()
|
||||
bot._mumble = None
|
||||
assert _find_channel(bot, "any") is None
|
||||
|
||||
|
||||
# -- TestChannelName ---------------------------------------------------------
|
||||
|
||||
|
||||
class TestChannelName:
|
||||
def test_resolves(self):
|
||||
lobby = _make_channel("Lobby", channel_id=0)
|
||||
bot = _FakeBot(channels={0: lobby})
|
||||
assert _channel_name(bot, 0) == "Lobby"
|
||||
|
||||
def test_missing_returns_id(self):
|
||||
bot = _FakeBot(channels={})
|
||||
assert _channel_name(bot, 42) == "42"
|
||||
|
||||
def test_no_mumble(self):
|
||||
bot = _FakeBot()
|
||||
bot._mumble = None
|
||||
assert _channel_name(bot, 5) == "5"
|
||||
|
||||
|
||||
# -- TestDispatch ------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDispatch:
|
||||
def test_no_args_shows_usage(self):
|
||||
bot = _FakeBot()
|
||||
msg = _FakeMessage(text="!mu")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert len(bot._replies) == 1
|
||||
assert "Usage" in bot._replies[0]
|
||||
|
||||
def test_unknown_sub_shows_usage(self):
|
||||
bot = _FakeBot()
|
||||
msg = _FakeMessage(text="!mu bogus")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "Usage" in bot._replies[0]
|
||||
|
||||
def test_valid_sub_routes(self):
|
||||
alice = _make_user("Alice")
|
||||
bot = _FakeBot(users={1: alice})
|
||||
msg = _FakeMessage(text="!mu kick Alice")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
alice.kick.assert_called_once_with("")
|
||||
assert "Kicked" in bot._replies[0]
|
||||
|
||||
|
||||
# -- TestKick ----------------------------------------------------------------
|
||||
|
||||
|
||||
class TestKick:
|
||||
def test_kick_user(self):
|
||||
alice = _make_user("Alice")
|
||||
bot = _FakeBot(users={1: alice})
|
||||
msg = _FakeMessage(text="!mu kick Alice")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
alice.kick.assert_called_once_with("")
|
||||
assert "Kicked Alice" in bot._replies[0]
|
||||
|
||||
def test_kick_with_reason(self):
|
||||
alice = _make_user("Alice")
|
||||
bot = _FakeBot(users={1: alice})
|
||||
msg = _FakeMessage(text="!mu kick Alice being rude")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
alice.kick.assert_called_once_with("being rude")
|
||||
|
||||
def test_kick_user_not_found(self):
|
||||
bot = _FakeBot(users={})
|
||||
msg = _FakeMessage(text="!mu kick Ghost")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "not found" in bot._replies[0].lower()
|
||||
|
||||
def test_kick_no_args(self):
|
||||
bot = _FakeBot()
|
||||
msg = _FakeMessage(text="!mu kick")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "Usage" in bot._replies[0]
|
||||
|
||||
|
||||
# -- TestBan -----------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBan:
|
||||
def test_ban_user(self):
|
||||
bob = _make_user("Bob")
|
||||
bot = _FakeBot(users={1: bob})
|
||||
msg = _FakeMessage(text="!mu ban Bob")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
bob.ban.assert_called_once_with("")
|
||||
assert "Banned Bob" in bot._replies[0]
|
||||
|
||||
def test_ban_with_reason(self):
|
||||
bob = _make_user("Bob")
|
||||
bot = _FakeBot(users={1: bob})
|
||||
msg = _FakeMessage(text="!mu ban Bob spamming")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
bob.ban.assert_called_once_with("spamming")
|
||||
|
||||
def test_ban_user_not_found(self):
|
||||
bot = _FakeBot(users={})
|
||||
msg = _FakeMessage(text="!mu ban Ghost")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "not found" in bot._replies[0].lower()
|
||||
|
||||
|
||||
# -- TestMuteUnmute ----------------------------------------------------------
|
||||
|
||||
|
||||
class TestMuteUnmute:
|
||||
def test_mute(self):
|
||||
alice = _make_user("Alice")
|
||||
bot = _FakeBot(users={1: alice})
|
||||
msg = _FakeMessage(text="!mu mute Alice")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
alice.mute.assert_called_once()
|
||||
assert "Muted" in bot._replies[0]
|
||||
|
||||
def test_unmute(self):
|
||||
alice = _make_user("Alice", mute=True)
|
||||
bot = _FakeBot(users={1: alice})
|
||||
msg = _FakeMessage(text="!mu unmute Alice")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
alice.unmute.assert_called_once()
|
||||
assert "Unmuted" in bot._replies[0]
|
||||
|
||||
def test_mute_not_found(self):
|
||||
bot = _FakeBot(users={})
|
||||
msg = _FakeMessage(text="!mu mute Nobody")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "not found" in bot._replies[0].lower()
|
||||
|
||||
def test_unmute_not_found(self):
|
||||
bot = _FakeBot(users={})
|
||||
msg = _FakeMessage(text="!mu unmute Nobody")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "not found" in bot._replies[0].lower()
|
||||
|
||||
|
||||
# -- TestDeafenUndeafen ------------------------------------------------------
|
||||
|
||||
|
||||
class TestDeafenUndeafen:
|
||||
def test_deafen(self):
|
||||
alice = _make_user("Alice")
|
||||
bot = _FakeBot(users={1: alice})
|
||||
msg = _FakeMessage(text="!mu deafen Alice")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
alice.deafen.assert_called_once()
|
||||
assert "Deafened" in bot._replies[0]
|
||||
|
||||
def test_undeafen(self):
|
||||
alice = _make_user("Alice", deaf=True)
|
||||
bot = _FakeBot(users={1: alice})
|
||||
msg = _FakeMessage(text="!mu undeafen Alice")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
alice.undeafen.assert_called_once()
|
||||
assert "Undeafened" in bot._replies[0]
|
||||
|
||||
def test_deafen_not_found(self):
|
||||
bot = _FakeBot(users={})
|
||||
msg = _FakeMessage(text="!mu deafen Nobody")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "not found" in bot._replies[0].lower()
|
||||
|
||||
def test_undeafen_not_found(self):
|
||||
bot = _FakeBot(users={})
|
||||
msg = _FakeMessage(text="!mu undeafen Nobody")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "not found" in bot._replies[0].lower()
|
||||
|
||||
|
||||
# -- TestMove ----------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMove:
|
||||
def test_move_user(self):
|
||||
alice = _make_user("Alice", channel_id=0)
|
||||
afk = _make_channel("AFK", channel_id=5)
|
||||
bot = _FakeBot(users={1: alice}, channels={0: _make_channel("Root"), 5: afk})
|
||||
msg = _FakeMessage(text="!mu move Alice AFK")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
alice.move_in.assert_called_once_with(5)
|
||||
assert "Moved Alice to AFK" in bot._replies[0]
|
||||
|
||||
def test_move_user_not_found(self):
|
||||
bot = _FakeBot(users={}, channels={5: _make_channel("AFK", channel_id=5)})
|
||||
msg = _FakeMessage(text="!mu move Ghost AFK")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "user not found" in bot._replies[0].lower()
|
||||
|
||||
def test_move_channel_not_found(self):
|
||||
alice = _make_user("Alice")
|
||||
bot = _FakeBot(users={1: alice}, channels={})
|
||||
msg = _FakeMessage(text="!mu move Alice Nowhere")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "channel not found" in bot._replies[0].lower()
|
||||
|
||||
def test_move_missing_args(self):
|
||||
bot = _FakeBot()
|
||||
msg = _FakeMessage(text="!mu move Alice")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "Usage" in bot._replies[0]
|
||||
|
||||
|
||||
# -- TestUsers ---------------------------------------------------------------
|
||||
|
||||
|
||||
class TestUsers:
|
||||
def test_list_users(self):
|
||||
alice = _make_user("Alice", channel_id=0)
|
||||
bob = _make_user("Bob", channel_id=0, self_mute=True)
|
||||
lobby = _make_channel("Lobby", channel_id=0)
|
||||
bot = _FakeBot(users={1: alice, 2: bob}, channels={0: lobby})
|
||||
msg = _FakeMessage(text="!mu users")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
reply = bot._replies[0]
|
||||
assert "2 user(s)" in reply
|
||||
assert "Alice" in reply
|
||||
assert "Bob" in reply
|
||||
assert "muted" in reply
|
||||
|
||||
def test_list_with_bots(self):
|
||||
alice = _make_user("Alice", channel_id=0)
|
||||
derp = _make_user("derp", channel_id=0)
|
||||
lobby = _make_channel("Lobby", channel_id=0)
|
||||
bot = _FakeBot(users={1: alice, 2: derp}, channels={0: lobby})
|
||||
bot.registry._bots = {"derp": MagicMock()}
|
||||
msg = _FakeMessage(text="!mu users")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
reply = bot._replies[0]
|
||||
assert "bot" in reply
|
||||
assert "2 user(s)" in reply
|
||||
|
||||
def test_deaf_flag(self):
|
||||
alice = _make_user("Alice", channel_id=0, self_deaf=True)
|
||||
lobby = _make_channel("Lobby", channel_id=0)
|
||||
bot = _FakeBot(users={1: alice}, channels={0: lobby})
|
||||
msg = _FakeMessage(text="!mu users")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "deaf" in bot._replies[0]
|
||||
|
||||
|
||||
# -- TestChannels ------------------------------------------------------------
|
||||
|
||||
|
||||
class TestChannels:
|
||||
def test_list_channels(self):
|
||||
lobby = _make_channel("Lobby", channel_id=0)
|
||||
afk = _make_channel("AFK", channel_id=1)
|
||||
alice = _make_user("Alice", channel_id=0)
|
||||
bot = _FakeBot(users={1: alice}, channels={0: lobby, 1: afk})
|
||||
msg = _FakeMessage(text="!mu channels")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
reply = bot._replies[0]
|
||||
assert "Lobby (1)" in reply
|
||||
assert "AFK (0)" in reply
|
||||
|
||||
|
||||
# -- TestMkchan --------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMkchan:
|
||||
def test_create_channel(self):
|
||||
bot = _FakeBot()
|
||||
msg = _FakeMessage(text="!mu mkchan Gaming")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
bot._mumble.channels.new_channel.assert_called_once_with(
|
||||
0, "Gaming", temporary=False,
|
||||
)
|
||||
assert "Created" in bot._replies[0]
|
||||
|
||||
def test_create_temp_channel(self):
|
||||
bot = _FakeBot()
|
||||
msg = _FakeMessage(text="!mu mkchan Gaming temp")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
bot._mumble.channels.new_channel.assert_called_once_with(
|
||||
0, "Gaming", temporary=True,
|
||||
)
|
||||
assert "temporary" in bot._replies[0]
|
||||
|
||||
def test_missing_name(self):
|
||||
bot = _FakeBot()
|
||||
msg = _FakeMessage(text="!mu mkchan")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "Usage" in bot._replies[0]
|
||||
|
||||
|
||||
# -- TestRmchan --------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRmchan:
|
||||
def test_remove_channel(self):
|
||||
afk = _make_channel("AFK", channel_id=5)
|
||||
bot = _FakeBot(channels={5: afk})
|
||||
msg = _FakeMessage(text="!mu rmchan AFK")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
afk.remove.assert_called_once()
|
||||
assert "Removed" in bot._replies[0]
|
||||
|
||||
def test_channel_not_found(self):
|
||||
bot = _FakeBot(channels={})
|
||||
msg = _FakeMessage(text="!mu rmchan Nowhere")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "not found" in bot._replies[0].lower()
|
||||
|
||||
def test_missing_args(self):
|
||||
bot = _FakeBot()
|
||||
msg = _FakeMessage(text="!mu rmchan")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "Usage" in bot._replies[0]
|
||||
|
||||
|
||||
# -- TestRename --------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRename:
|
||||
def test_rename_channel(self):
|
||||
afk = _make_channel("AFK", channel_id=5)
|
||||
bot = _FakeBot(channels={5: afk})
|
||||
msg = _FakeMessage(text="!mu rename AFK Chill")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
afk.rename_channel.assert_called_once_with("Chill")
|
||||
assert "Renamed" in bot._replies[0]
|
||||
|
||||
def test_channel_not_found(self):
|
||||
bot = _FakeBot(channels={})
|
||||
msg = _FakeMessage(text="!mu rename Nowhere New")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "not found" in bot._replies[0].lower()
|
||||
|
||||
def test_missing_args(self):
|
||||
bot = _FakeBot()
|
||||
msg = _FakeMessage(text="!mu rename AFK")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "Usage" in bot._replies[0]
|
||||
|
||||
|
||||
# -- TestDesc ----------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDesc:
|
||||
def test_set_description(self):
|
||||
afk = _make_channel("AFK", channel_id=5)
|
||||
bot = _FakeBot(channels={5: afk})
|
||||
msg = _FakeMessage(text="!mu desc AFK Away from keyboard")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
afk.set_channel_description.assert_called_once_with("Away from keyboard")
|
||||
assert "description" in bot._replies[0].lower()
|
||||
|
||||
def test_channel_not_found(self):
|
||||
bot = _FakeBot(channels={})
|
||||
msg = _FakeMessage(text="!mu desc Nowhere some text")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "not found" in bot._replies[0].lower()
|
||||
|
||||
def test_missing_args(self):
|
||||
bot = _FakeBot()
|
||||
msg = _FakeMessage(text="!mu desc AFK")
|
||||
asyncio.run(cmd_mu(bot, msg))
|
||||
assert "Usage" in bot._replies[0]
|
||||
Reference in New Issue
Block a user