292 lines
8.6 KiB
Python
292 lines
8.6 KiB
Python
"""Tests for player alias system."""
|
|
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from mudlib.commands import CommandDefinition, dispatch, register
|
|
from mudlib.player import Player
|
|
from mudlib.store import (
|
|
create_account,
|
|
init_db,
|
|
load_aliases,
|
|
save_aliases,
|
|
save_player,
|
|
)
|
|
|
|
|
|
# Persistence tests
|
|
def test_save_and_load_aliases_roundtrip():
|
|
"""Save and load aliases from database."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
db_path = Path(tmpdir) / "test.db"
|
|
init_db(db_path)
|
|
|
|
aliases = {"PR": "punch right", "pl": "punch left", "l": "look"}
|
|
save_aliases("goku", aliases, db_path)
|
|
|
|
loaded = load_aliases("goku", db_path)
|
|
assert loaded == {"pr": "punch right", "pl": "punch left", "l": "look"}
|
|
|
|
|
|
def test_load_aliases_empty():
|
|
"""Loading aliases for player with none returns empty dict."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
db_path = Path(tmpdir) / "test.db"
|
|
init_db(db_path)
|
|
|
|
loaded = load_aliases("goku", db_path)
|
|
assert loaded == {}
|
|
|
|
|
|
def test_save_aliases_overwrites_existing():
|
|
"""Saving new aliases replaces old ones."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
db_path = Path(tmpdir) / "test.db"
|
|
init_db(db_path)
|
|
|
|
save_aliases("goku", {"pr": "punch right"}, db_path)
|
|
save_aliases("goku", {"pl": "punch left", "l": "look"}, db_path)
|
|
|
|
loaded = load_aliases("goku", db_path)
|
|
assert loaded == {"pl": "punch left", "l": "look"}
|
|
assert "pr" not in loaded
|
|
|
|
|
|
# Alias command tests
|
|
@pytest.mark.asyncio
|
|
async def test_alias_list_empty(player):
|
|
"""alias with no args shows message when no aliases defined."""
|
|
from mudlib.commands.alias import cmd_alias
|
|
|
|
await cmd_alias(player, "")
|
|
player.writer.write.assert_called_with("No aliases defined.\r\n")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alias_create(player):
|
|
"""alias <name> <expansion> creates an alias."""
|
|
from mudlib.commands.alias import cmd_alias
|
|
|
|
await cmd_alias(player, "pr punch right")
|
|
assert player.aliases["pr"] == "punch right"
|
|
player.writer.write.assert_called_with("Alias set: pr -> punch right\r\n")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alias_normalizes_name_to_lowercase(player):
|
|
"""Alias names are normalized so dispatch lookup is consistent."""
|
|
from mudlib.commands.alias import cmd_alias
|
|
|
|
await cmd_alias(player, "PR punch right")
|
|
assert "pr" in player.aliases
|
|
assert "PR" not in player.aliases
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alias_list_with_aliases(player):
|
|
"""alias with no args lists all aliases."""
|
|
from mudlib.commands.alias import cmd_alias
|
|
|
|
player.aliases = {"pr": "punch right", "pl": "punch left", "l": "look"}
|
|
|
|
await cmd_alias(player, "")
|
|
output = player.writer.write.call_args[0][0]
|
|
assert "pr -> punch right" in output
|
|
assert "pl -> punch left" in output
|
|
assert "l -> look" in output
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alias_show_single(player):
|
|
"""alias <name> shows that specific alias."""
|
|
from mudlib.commands.alias import cmd_alias
|
|
|
|
player.aliases = {"pr": "punch right", "pl": "punch left"}
|
|
|
|
await cmd_alias(player, "pr")
|
|
player.writer.write.assert_called_with("pr -> punch right\r\n")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unalias_removes_alias(player):
|
|
"""unalias <name> removes an alias."""
|
|
from mudlib.commands.alias import cmd_unalias
|
|
|
|
player.aliases = {"pr": "punch right"}
|
|
|
|
await cmd_unalias(player, "pr")
|
|
assert "pr" not in player.aliases
|
|
player.writer.write.assert_called_with("Alias removed: pr\r\n")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unalias_is_case_insensitive(player):
|
|
"""unalias should remove aliases regardless of case."""
|
|
from mudlib.commands.alias import cmd_unalias
|
|
|
|
player.aliases = {"pr": "punch right"}
|
|
await cmd_unalias(player, "PR")
|
|
assert "pr" not in player.aliases
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unalias_no_such_alias(player):
|
|
"""unalias on non-existent alias shows error."""
|
|
from mudlib.commands.alias import cmd_unalias
|
|
|
|
await cmd_unalias(player, "pr")
|
|
player.writer.write.assert_called_with("No such alias: pr\r\n")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alias_cannot_override_builtin(player):
|
|
"""Cannot alias over existing built-in commands."""
|
|
import mudlib.commands.look # noqa: F401 - needed to register look command
|
|
from mudlib.commands.alias import cmd_alias
|
|
|
|
await cmd_alias(player, "look punch right")
|
|
player.writer.write.assert_called_with(
|
|
"Cannot alias over built-in command: look\r\n"
|
|
)
|
|
assert "look" not in player.aliases
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alias_builtin_collision_is_case_insensitive(player):
|
|
"""Built-in collision checks should apply regardless of alias casing."""
|
|
import mudlib.commands.look # noqa: F401 - needed to register look command
|
|
from mudlib.commands.alias import cmd_alias
|
|
|
|
await cmd_alias(player, "LOOK punch right")
|
|
player.writer.write.assert_called_with(
|
|
"Cannot alias over built-in command: look\r\n"
|
|
)
|
|
|
|
|
|
# Dispatch integration tests
|
|
@pytest.mark.asyncio
|
|
async def test_alias_expands_in_dispatch(player):
|
|
"""Aliases are expanded before command dispatch."""
|
|
called_with = []
|
|
|
|
async def test_handler(p, args):
|
|
called_with.append(args)
|
|
|
|
register(CommandDefinition("testcmd", test_handler))
|
|
player.aliases["tc"] = "testcmd"
|
|
|
|
await dispatch(player, "tc hello")
|
|
assert called_with == ["hello"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alias_expands_in_dispatch_case_insensitive_key(player):
|
|
"""Stored aliases with uppercase keys still load/use as lowercase."""
|
|
called_with = []
|
|
|
|
async def test_handler(p, args):
|
|
called_with.append(args)
|
|
|
|
register(CommandDefinition("testcmd", test_handler))
|
|
player.aliases["pr"] = "testcmd"
|
|
|
|
await dispatch(player, "PR hello")
|
|
assert called_with == ["hello"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alias_with_extra_args(player):
|
|
"""Alias expansion preserves additional arguments."""
|
|
called_with = []
|
|
|
|
async def test_handler(p, args):
|
|
called_with.append(args)
|
|
|
|
register(CommandDefinition("testcmd", test_handler))
|
|
player.aliases["tc"] = "testcmd arg1"
|
|
|
|
await dispatch(player, "tc arg2 arg3")
|
|
# Expansion: "testcmd arg1" + " arg2 arg3" = "testcmd arg1 arg2 arg3"
|
|
assert called_with == ["arg1 arg2 arg3"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_nested_alias_max_depth(player):
|
|
"""Nested aliases are limited to prevent infinite recursion."""
|
|
called = []
|
|
|
|
async def test_handler(p, args):
|
|
called.append(True)
|
|
|
|
register(CommandDefinition("final", test_handler))
|
|
|
|
# Create a chain: a -> b -> c -> ... -> final
|
|
player.aliases["a"] = "b"
|
|
player.aliases["b"] = "c"
|
|
player.aliases["c"] = "d"
|
|
player.aliases["d"] = "e"
|
|
player.aliases["e"] = "f"
|
|
player.aliases["f"] = "g"
|
|
player.aliases["g"] = "h"
|
|
player.aliases["h"] = "i"
|
|
player.aliases["i"] = "j"
|
|
player.aliases["j"] = "final"
|
|
|
|
await dispatch(player, "a")
|
|
assert len(called) == 1 # Should complete successfully
|
|
|
|
# Now create an 11-deep chain that exceeds limit
|
|
player.aliases["j"] = "k"
|
|
player.aliases["k"] = "final"
|
|
|
|
called.clear()
|
|
await dispatch(player, "a")
|
|
# Should fail and send error message
|
|
assert len(called) == 0
|
|
assert "Too many nested aliases" in player.writer.write.call_args[0][0]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unknown_alias_falls_through(player):
|
|
"""Unknown aliases fall through to normal command resolution."""
|
|
player.aliases = {} # No aliases defined
|
|
|
|
# This should hit normal "Unknown command" path
|
|
await dispatch(player, "nonexistent")
|
|
assert "Unknown command" in player.writer.write.call_args[0][0]
|
|
|
|
|
|
# Integration test for persistence
|
|
def test_aliases_persist_on_save_player():
|
|
"""Aliases are saved when save_player is called."""
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".db") as f:
|
|
db_path = f.name
|
|
|
|
try:
|
|
init_db(db_path)
|
|
create_account("Goku", "password")
|
|
|
|
# Create player with aliases
|
|
mock_writer = MagicMock()
|
|
player = Player(
|
|
name="Goku",
|
|
x=10,
|
|
y=20,
|
|
reader=MagicMock(),
|
|
writer=mock_writer,
|
|
)
|
|
player.aliases = {"pr": "punch right", "pl": "punch left", "l": "look"}
|
|
|
|
# Save player (should save aliases too)
|
|
save_player(player)
|
|
|
|
# Load aliases directly from database
|
|
loaded = load_aliases("Goku")
|
|
assert loaded == {"pr": "punch right", "pl": "punch left", "l": "look"}
|
|
|
|
finally:
|
|
os.unlink(db_path)
|