"""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 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