"""Tests for content loading from TOML files.""" import logging import tempfile from pathlib import Path from unittest.mock import AsyncMock, MagicMock import pytest from mudlib.content import load_command, load_commands from mudlib.player import Player @pytest.fixture def mock_writer(): writer = MagicMock() writer.write = MagicMock() writer.drain = AsyncMock() return writer @pytest.fixture def mock_reader(): return MagicMock() @pytest.fixture def player(mock_reader, mock_writer): return Player(name="TestPlayer", x=5, y=5, reader=mock_reader, writer=mock_writer) def test_load_command_with_handler_reference(): """Test loading a command with a Python handler reference.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f: f.write(""" name = "shout" aliases = ["yell"] help = "shout a message to nearby players" mode = "normal" handler = "mudlib.commands.look:cmd_look" """) f.flush() tmp_path = Path(f.name) try: defn = load_command(tmp_path) assert defn.name == "shout" assert defn.aliases == ["yell"] assert defn.help == "shout a message to nearby players" assert defn.mode == "normal" assert callable(defn.handler) finally: tmp_path.unlink() def test_load_command_with_message(): """Test loading a command that just sends a message.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f: f.write(""" name = "rules" aliases = ["info"] help = "display the server rules" mode = "*" message = \"\"\" Welcome to the MUD! 1. Be respectful 2. No cheating \"\"\" """) f.flush() tmp_path = Path(f.name) try: defn = load_command(tmp_path) assert defn.name == "rules" assert defn.aliases == ["info"] assert defn.help == "display the server rules" assert defn.mode == "*" assert callable(defn.handler) finally: tmp_path.unlink() @pytest.mark.asyncio async def test_message_command_sends_text(player, mock_writer): """Test that a message command actually sends the text to the player.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f: f.write(""" name = "motd" message = "Welcome to the server!\\nEnjoy your stay." """) f.flush() tmp_path = Path(f.name) try: defn = load_command(tmp_path) await defn.handler(player, "") assert mock_writer.write.called written_text = mock_writer.write.call_args[0][0] assert "Welcome to the server!" in written_text assert "Enjoy your stay." in written_text finally: tmp_path.unlink() def test_load_commands_from_directory(): """Test loading multiple command definitions from a directory.""" with tempfile.TemporaryDirectory() as tmpdir: tmp_path = Path(tmpdir) # Create two command files (tmp_path / "cmd1.toml").write_text(""" name = "cmd1" message = "Command 1" """) (tmp_path / "cmd2.toml").write_text(""" name = "cmd2" message = "Command 2" """) # Create a non-toml file that should be ignored (tmp_path / "readme.txt").write_text("This is not a command") commands = load_commands(tmp_path) assert len(commands) == 2 names = {cmd.name for cmd in commands} assert names == {"cmd1", "cmd2"} def test_load_command_missing_name(): """Test that missing name field raises an error.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f: f.write(""" message = "Hello" """) f.flush() tmp_path = Path(f.name) try: with pytest.raises(KeyError): load_command(tmp_path) finally: tmp_path.unlink() def test_load_command_missing_handler_and_message(): """Test that a command without handler or message raises an error.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f: f.write(""" name = "broken" """) f.flush() tmp_path = Path(f.name) try: match_msg = "must specify either 'handler' or 'message'" with pytest.raises(ValueError, match=match_msg): load_command(tmp_path) finally: tmp_path.unlink() def test_load_command_bad_handler_reference(): """Test that an invalid handler reference raises an error.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f: f.write(""" name = "broken" handler = "nonexistent.module:function" """) f.flush() tmp_path = Path(f.name) try: with pytest.raises((ImportError, ModuleNotFoundError)): load_command(tmp_path) finally: tmp_path.unlink() def test_load_command_defaults(): """Test that optional fields have sensible defaults.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f: f.write(""" name = "simple" message = "Hello" """) f.flush() tmp_path = Path(f.name) try: defn = load_command(tmp_path) assert defn.name == "simple" assert defn.aliases == [] assert defn.help == "" assert defn.mode == "normal" finally: tmp_path.unlink() def test_load_command_both_handler_and_message(): """Test that specifying both handler and message raises an error.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f: f.write(""" name = "broken" handler = "mudlib.commands.look:cmd_look" message = "Hello" """) f.flush() tmp_path = Path(f.name) try: match_msg = "cannot specify both 'handler' and 'message'" with pytest.raises(ValueError, match=match_msg): load_command(tmp_path) finally: tmp_path.unlink() def test_load_command_handler_wrong_type(): """Test that non-string handler field raises TypeError.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f: f.write(""" name = "broken" handler = 123 """) f.flush() tmp_path = Path(f.name) try: match_msg = "'handler' must be a string" with pytest.raises(TypeError, match=match_msg): load_command(tmp_path) finally: tmp_path.unlink() def test_load_command_message_wrong_type(): """Test that non-string message field raises TypeError.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f: f.write(""" name = "broken" message = 456 """) f.flush() tmp_path = Path(f.name) try: match_msg = "'message' must be a string" with pytest.raises(TypeError, match=match_msg): load_command(tmp_path) finally: tmp_path.unlink() @pytest.mark.asyncio async def test_message_whitespace_handling(player, mock_writer): """Test message stripping removes outer whitespace, keeps internal formatting.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f: f.write(""" name = "formatted" message = \"\"\" First line Indented line Last line \"\"\" """) f.flush() tmp_path = Path(f.name) try: defn = load_command(tmp_path) await defn.handler(player, "") assert mock_writer.write.called written_text = mock_writer.write.call_args[0][0] # Outer whitespace should be stripped assert not written_text.startswith("\n") # But internal formatting should be preserved assert "First line\n Indented line\nLast line" in written_text finally: tmp_path.unlink() def test_load_commands_logs_broken_files(caplog): """Test that load_commands logs warnings for broken TOML files.""" with tempfile.TemporaryDirectory() as tmpdir: tmp_path = Path(tmpdir) # Create a valid command file (tmp_path / "valid.toml").write_text(""" name = "valid" message = "OK" """) # Create a broken command file (missing required field) (tmp_path / "broken.toml").write_text(""" name = "broken" """) with caplog.at_level(logging.WARNING): commands = load_commands(tmp_path) # Should load the valid one only assert len(commands) == 1 assert commands[0].name == "valid" # Should log warning about the broken file warnings = [r.message for r in caplog.records] assert any("failed to load command from" in msg for msg in warnings) assert any("broken.toml" in msg for msg in warnings)