mud/tests/test_build_commands.py

327 lines
8.5 KiB
Python

"""Tests for builder commands."""
from unittest.mock import AsyncMock, MagicMock
import pytest
from mudlib.player import Player, players
from mudlib.zone import Zone
from mudlib.zones import get_zone, register_zone, zone_registry
@pytest.fixture(autouse=True)
def clear_state():
players.clear()
zone_registry.clear()
yield
players.clear()
zone_registry.clear()
@pytest.fixture
def zone():
terrain = [["." for _ in range(10)] for _ in range(10)]
z = Zone(name="hub", width=10, height=10, terrain=terrain)
register_zone("hub", z)
return z
@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(zone, mock_writer, mock_reader):
p = Player(
name="builder",
x=5,
y=5,
writer=mock_writer,
reader=mock_reader,
location=zone,
is_admin=True,
)
zone._contents.append(p)
players["builder"] = p
return p
# --- @goto ---
@pytest.mark.asyncio
async def test_goto_existing_zone(player):
"""@goto teleports player to named zone's spawn point."""
from mudlib.commands.build import cmd_goto
target = Zone(
name="forest",
width=5,
height=5,
terrain=[["." for _ in range(5)] for _ in range(5)],
spawn_x=2,
spawn_y=3,
)
register_zone("forest", target)
await cmd_goto(player, "forest")
assert player.location is target
assert player.x == 2
assert player.y == 3
@pytest.mark.asyncio
async def test_goto_nonexistent_zone(player, mock_writer):
"""@goto with unknown zone name shows error."""
from mudlib.commands.build import cmd_goto
await cmd_goto(player, "nowhere")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[-1][0][0]
assert "not found" in written.lower() or "no zone" in written.lower()
@pytest.mark.asyncio
async def test_goto_no_args(player, mock_writer):
"""@goto without arguments shows usage."""
from mudlib.commands.build import cmd_goto
await cmd_goto(player, "")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[0][0][0]
assert "usage" in written.lower() or "goto" in written.lower()
# --- @dig ---
@pytest.mark.asyncio
async def test_dig_creates_zone(player):
"""@dig creates a new zone and teleports player there."""
from mudlib.commands.build import cmd_dig
await cmd_dig(player, "cave 8 6")
new_zone = get_zone("cave")
assert new_zone is not None
assert new_zone.width == 8
assert new_zone.height == 6
assert player.location is new_zone
assert player.x == 0
assert player.y == 0
@pytest.mark.asyncio
async def test_dig_creates_terrain(player):
"""@dig creates terrain filled with '.' tiles."""
from mudlib.commands.build import cmd_dig
await cmd_dig(player, "mine 5 4")
new_zone = get_zone("mine")
assert new_zone is not None
assert len(new_zone.terrain) == 4
assert len(new_zone.terrain[0]) == 5
assert all(tile == "." for row in new_zone.terrain for tile in row)
@pytest.mark.asyncio
async def test_dig_zone_not_toroidal(player):
"""@dig creates bounded (non-toroidal) zones."""
from mudlib.commands.build import cmd_dig
await cmd_dig(player, "room 3 3")
new_zone = get_zone("room")
assert new_zone is not None
assert new_zone.toroidal is False
@pytest.mark.asyncio
async def test_dig_existing_name(player, mock_writer):
"""@dig with existing zone name shows error."""
from mudlib.commands.build import cmd_dig
await cmd_dig(player, "hub 5 5")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[-1][0][0]
assert "already exists" in written.lower()
@pytest.mark.asyncio
async def test_dig_bad_args(player, mock_writer):
"""@dig with wrong number of args shows usage."""
from mudlib.commands.build import cmd_dig
await cmd_dig(player, "cave")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[0][0][0]
assert "usage" in written.lower() or "dig" in written.lower()
@pytest.mark.asyncio
async def test_dig_non_numeric_dimensions(player, mock_writer):
"""@dig with non-numeric dimensions shows error."""
from mudlib.commands.build import cmd_dig
await cmd_dig(player, "cave abc def")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[0][0][0]
assert "numbers" in written.lower()
@pytest.mark.asyncio
async def test_dig_zero_or_negative_dimensions(player, mock_writer):
"""@dig with zero or negative dimensions shows error."""
from mudlib.commands.build import cmd_dig
await cmd_dig(player, "cave 0 5")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[0][0][0]
assert "at least 1x1" in written.lower()
mock_writer.write.reset_mock()
await cmd_dig(player, "cave 5 -2")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[0][0][0]
assert "at least 1x1" in written.lower()
# --- @save ---
@pytest.mark.asyncio
async def test_save_zone(player, zone, tmp_path, mock_writer):
"""@save writes zone to TOML file."""
from mudlib.commands.build import cmd_save, set_content_dir
set_content_dir(tmp_path)
zones_dir = tmp_path / "zones"
zones_dir.mkdir()
await cmd_save(player, "")
# Check file was created
saved_file = zones_dir / "hub.toml"
assert saved_file.exists()
content = saved_file.read_text()
assert 'name = "hub"' in content
# Check confirmation message
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[-1][0][0]
assert "saved" in written.lower()
# --- @place ---
@pytest.mark.asyncio
async def test_place_thing(player, zone, mock_writer):
"""@place puts a thing at player's position."""
from mudlib.commands.build import cmd_place
from mudlib.things import ThingTemplate, thing_templates
thing_templates["sign"] = ThingTemplate(
name="sign",
description="a wooden sign",
)
await cmd_place(player, "sign")
# Check thing is in zone at player's position
from mudlib.thing import Thing
things = [
obj
for obj in zone.contents_at(5, 5)
if isinstance(obj, Thing) and obj.name == "sign"
]
assert len(things) == 1
assert things[0].location is zone
# Clean up
del thing_templates["sign"]
@pytest.mark.asyncio
async def test_place_unknown_thing(player, mock_writer):
"""@place with unknown thing name shows error."""
from mudlib.commands.build import cmd_place
await cmd_place(player, "unicorn")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[0][0][0]
assert "not found" in written.lower() or "unknown" in written.lower()
@pytest.mark.asyncio
async def test_place_no_args(player, mock_writer):
"""@place without arguments shows usage."""
from mudlib.commands.build import cmd_place
await cmd_place(player, "")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[0][0][0]
assert "usage" in written.lower() or "place" in written.lower()
# --- Permission checks ---
@pytest.mark.asyncio
async def test_builder_commands_require_admin(zone, mock_writer, mock_reader):
"""Non-admin players cannot use builder commands."""
from mudlib.commands import dispatch
non_admin = Player(
name="player",
x=5,
y=5,
writer=mock_writer,
reader=mock_reader,
location=zone,
is_admin=False,
)
await dispatch(non_admin, "@goto hub")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[-1][0][0]
assert "permission" in written.lower()
mock_writer.write.reset_mock()
await dispatch(non_admin, "@dig test 5 5")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[-1][0][0]
assert "permission" in written.lower()
mock_writer.write.reset_mock()
await dispatch(non_admin, "@save")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[-1][0][0]
assert "permission" in written.lower()
mock_writer.write.reset_mock()
await dispatch(non_admin, "@place thing")
mock_writer.write.assert_called()
written = mock_writer.write.call_args_list[-1][0][0]
assert "permission" in written.lower()