332 lines
9.8 KiB
Python
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
|