mud/tests/test_admin.py

332 lines
9.8 KiB
Python

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