From 9eaca966c8df132612ed0e3a6d31f52f23e404c9 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Sat, 14 Feb 2026 18:43:09 -0500 Subject: [PATCH] Add tests for admin system --- tests/test_admin.py | 332 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 tests/test_admin.py diff --git a/tests/test_admin.py b/tests/test_admin.py new file mode 100644 index 0000000..1e248d3 --- /dev/null +++ b/tests/test_admin.py @@ -0,0 +1,332 @@ +"""Tests for the admin system.""" + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from mudlib.commands.build import cmd_demote, cmd_promote +from mudlib.player import Player, players +from mudlib.store import ( + create_account, + init_db, + load_player_data, + set_admin, +) +from mudlib.zone import Zone +from mudlib.zones import 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 db(tmp_path): + init_db(tmp_path / "test.db") + + +@pytest.fixture +def zone(): + terrain = [["." for _ in range(10)] for _ in range(10)] + z = Zone(name="testzone", width=10, height=10, terrain=terrain) + register_zone("testzone", 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() + + +def make_player(name, zone, mock_writer, mock_reader, is_admin=False): + p = Player( + name=name, + x=5, + y=5, + writer=mock_writer, + reader=mock_reader, + location=zone, + is_admin=is_admin, + ) + zone._contents.append(p) + players[name] = p + return p + + +# Store layer tests + + +@pytest.mark.asyncio +async def test_first_account_is_admin(db): + """First account created becomes admin.""" + create_account("first", "password") + data = load_player_data("first") + assert data is not None + assert data["is_admin"] is True + + +@pytest.mark.asyncio +async def test_second_account_not_admin(db): + """Second account created is not admin.""" + create_account("first", "password") + create_account("second", "password") + + first_data = load_player_data("first") + second_data = load_player_data("second") + + assert first_data is not None + assert first_data["is_admin"] is True + assert second_data is not None + assert second_data["is_admin"] is False + + +@pytest.mark.asyncio +async def test_set_admin(db): + """set_admin grants admin status to non-admin account.""" + create_account("first", "password") + create_account("second", "password") + + # Second account should not be admin initially + data = load_player_data("second") + assert data is not None + assert data["is_admin"] is False + + # Promote second account + result = set_admin("second", True) + assert result is True + + # Verify admin status + data = load_player_data("second") + assert data is not None + assert data["is_admin"] is True + + +@pytest.mark.asyncio +async def test_set_admin_nonexistent(db): + """set_admin returns False for nonexistent account.""" + result = set_admin("nobody", True) + assert result is False + + +@pytest.mark.asyncio +async def test_migration_promotes_earliest(db): + """Migration promotes earliest account when is_admin column added.""" + # Create two accounts + create_account("alice", "password") + create_account("bob", "password") + + # Verify first account is admin + alice_data = load_player_data("alice") + bob_data = load_player_data("bob") + + assert alice_data is not None + assert alice_data["is_admin"] is True + assert bob_data is not None + assert bob_data["is_admin"] is False + + +# Command tests + + +@pytest.mark.asyncio +async def test_promote_no_args(zone, mock_writer, mock_reader, db): + """promote with no args shows usage.""" + player = make_player("builder", zone, mock_writer, mock_reader, is_admin=True) + create_account("builder", "password") + + await cmd_promote(player, "") + + output = "".join(call.args[0] for call in player.writer.write.call_args_list) + assert "Usage:" in output + + +@pytest.mark.asyncio +async def test_promote_nonexistent(zone, mock_writer, mock_reader, db): + """promote nonexistent account shows error.""" + player = make_player("builder", zone, mock_writer, mock_reader, is_admin=True) + create_account("builder", "password") + + await cmd_promote(player, "nobody") + + output = "".join(call.args[0] for call in player.writer.write.call_args_list) + assert "not found" in output + + +@pytest.mark.asyncio +async def test_promote_offline_player(zone, mock_writer, mock_reader, db): + """promote offline player updates database.""" + player = make_player("builder", zone, mock_writer, mock_reader, is_admin=True) + create_account("builder", "password") + create_account("target", "password") + + await cmd_promote(player, "target") + + # Verify database updated + data = load_player_data("target") + assert data is not None + assert data["is_admin"] is True + + output = "".join(call.args[0] for call in player.writer.write.call_args_list) + assert "is now an admin" in output + + +@pytest.mark.asyncio +async def test_promote_online_player(zone, mock_writer, mock_reader, db): + """promote online player updates both database and live state.""" + player = make_player("builder", zone, mock_writer, mock_reader, is_admin=True) + create_account("builder", "password") + create_account("target", "password") + + # Create target as online player with separate mock_writer + target_writer = MagicMock() + target_writer.write = MagicMock() + target_writer.drain = AsyncMock() + target = make_player("target", zone, target_writer, mock_reader, is_admin=False) + + await cmd_promote(player, "target") + + # Verify live state updated + assert target.is_admin is True + + # Verify database updated + data = load_player_data("target") + assert data is not None + assert data["is_admin"] is True + + # Verify messages sent + builder_output = "".join( + call.args[0] for call in player.writer.write.call_args_list + ) + assert "is now an admin" in builder_output + + target_output = "".join(call.args[0] for call in target_writer.write.call_args_list) + assert "granted admin status" in target_output + + +@pytest.mark.asyncio +async def test_demote_self(zone, mock_writer, mock_reader, db): + """demote self shows error.""" + player = make_player("builder", zone, mock_writer, mock_reader, is_admin=True) + create_account("builder", "password") + + await cmd_demote(player, "builder") + + output = "".join(call.args[0] for call in player.writer.write.call_args_list) + assert "can't demote yourself" in output + + +@pytest.mark.asyncio +async def test_demote_online_player(zone, mock_writer, mock_reader, db): + """demote online player updates both database and live state.""" + player = make_player("builder", zone, mock_writer, mock_reader, is_admin=True) + create_account("builder", "password") + create_account("target", "password") + set_admin("target", True) + + # Create target as online admin player with separate mock_writer + target_writer = MagicMock() + target_writer.write = MagicMock() + target_writer.drain = AsyncMock() + target = make_player("target", zone, target_writer, mock_reader, is_admin=True) + + await cmd_demote(player, "target") + + # Verify live state updated + assert target.is_admin is False + + # Verify database updated + data = load_player_data("target") + assert data is not None + assert data["is_admin"] is False + + # Verify messages sent + builder_output = "".join( + call.args[0] for call in player.writer.write.call_args_list + ) + assert "no longer an admin" in builder_output + + target_output = "".join(call.args[0] for call in target_writer.write.call_args_list) + assert "revoked" in target_output + + +@pytest.mark.asyncio +async def test_demote_no_args(zone, mock_writer, mock_reader, db): + """demote with no args shows usage.""" + player = make_player("builder", zone, mock_writer, mock_reader, is_admin=True) + create_account("builder", "password") + + await cmd_demote(player, "") + + output = "".join(call.args[0] for call in player.writer.write.call_args_list) + assert "Usage:" in output + + +@pytest.mark.asyncio +async def test_demote_offline_player(zone, mock_writer, mock_reader, db): + """demote offline player updates database.""" + admin_player = make_player("admin", zone, mock_writer, mock_reader, is_admin=True) + create_account("admin", "password") + create_account("target", "password") + set_admin("target", True) + + await cmd_demote(admin_player, "target") + + # Verify database updated + data = load_player_data("target") + assert data is not None + assert data["is_admin"] is False + + output = "".join(call.args[0] for call in admin_player.writer.write.call_args_list) + assert "no longer an admin" in output + + +@pytest.mark.asyncio +async def test_demote_nonexistent(zone, mock_writer, mock_reader, db): + """demote nonexistent account shows error.""" + player = make_player("builder", zone, mock_writer, mock_reader, is_admin=True) + create_account("builder", "password") + + await cmd_demote(player, "nobody") + + output = "".join(call.args[0] for call in player.writer.write.call_args_list) + assert "not found" in output + + +@pytest.mark.asyncio +async def test_promote_already_admin(zone, mock_writer, mock_reader, db): + """promote already-admin account shows message.""" + player = make_player("builder", zone, mock_writer, mock_reader, is_admin=True) + create_account("builder", "password") + create_account("target", "password") + set_admin("target", True) + + await cmd_promote(player, "target") + + output = "".join(call.args[0] for call in player.writer.write.call_args_list) + assert "already an admin" in output + + +@pytest.mark.asyncio +async def test_demote_not_admin(zone, mock_writer, mock_reader, db): + """demote non-admin account shows message.""" + player = make_player("builder", zone, mock_writer, mock_reader, is_admin=True) + create_account("builder", "password") + create_account("target", "password") + + await cmd_demote(player, "target") + + output = "".join(call.args[0] for call in player.writer.write.call_args_list) + assert "not an admin" in output