golfgame/server/test_room.py
adlee-was-taken 9fc6b83bba v3.0.0: V3 features, server refactoring, and documentation overhaul
- Extract WebSocket handlers from main.py into handlers.py
- Add V3 feature docs (dealer rotation, dealing animation, round end reveal,
  column pair celebration, final turn urgency, opponent thinking, score tallying,
  card hover/selection, knock early drama, column pair indicator, swap animation
  improvements, draw source distinction, card value tooltips, active rules context,
  discard pile history, realistic card sounds)
- Add V3 refactoring docs (ai.py, main.py/game.py, misc improvements)
- Add installation guide with Docker, systemd, and nginx setup
- Add helper scripts (install.sh, dev-server.sh, docker-build.sh)
- Add animation flow diagrams documentation
- Add test files for handlers, rooms, and V3 features
- Add e2e test specs for V3 features
- Update README with complete project structure and current tech stack
- Update CLAUDE.md with full architecture tree and server layer descriptions
- Update .env.example to reflect PostgreSQL (remove SQLite references)
- Update .gitignore to exclude virtualenv files, .claude/, and .db files
- Remove tracked virtualenv files (bin/, lib64, pyvenv.cfg)
- Remove obsolete game_log.py (SQLite) and games.db

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 10:03:45 -05:00

318 lines
10 KiB
Python

"""
Test suite for Room and RoomManager CRUD operations.
Covers:
- Room creation and uniqueness
- Player add/remove with host reassignment
- CPU player management
- Case-insensitive room lookup
- Cross-room player search
- Message broadcast and send_to
Run with: pytest test_room.py -v
"""
import asyncio
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from room import Room, RoomPlayer, RoomManager
# =============================================================================
# Mock helpers
# =============================================================================
class MockWebSocket:
"""Mock WebSocket that collects sent messages."""
def __init__(self):
self.messages: list[dict] = []
async def send_json(self, data: dict):
self.messages.append(data)
# =============================================================================
# RoomManager tests
# =============================================================================
class TestRoomManagerCreate:
def test_create_room_returns_room(self):
rm = RoomManager()
room = rm.create_room()
assert room is not None
assert len(room.code) == 4
assert room.code in rm.rooms
def test_create_multiple_rooms_unique_codes(self):
rm = RoomManager()
codes = set()
for _ in range(20):
room = rm.create_room()
codes.add(room.code)
assert len(codes) == 20
def test_remove_room(self):
rm = RoomManager()
room = rm.create_room()
code = room.code
rm.remove_room(code)
assert code not in rm.rooms
def test_remove_nonexistent_room(self):
rm = RoomManager()
rm.remove_room("ZZZZ") # Should not raise
class TestRoomManagerLookup:
def test_get_room_case_insensitive(self):
rm = RoomManager()
room = rm.create_room()
code = room.code
assert rm.get_room(code.lower()) is room
assert rm.get_room(code.upper()) is room
def test_get_room_not_found(self):
rm = RoomManager()
assert rm.get_room("ZZZZ") is None
def test_find_player_room(self):
rm = RoomManager()
room = rm.create_room()
ws = MockWebSocket()
room.add_player("player1", "Alice", ws)
found = rm.find_player_room("player1")
assert found is room
def test_find_player_room_not_found(self):
rm = RoomManager()
rm.create_room()
assert rm.find_player_room("nobody") is None
def test_find_player_room_cross_room(self):
rm = RoomManager()
room1 = rm.create_room()
room2 = rm.create_room()
room1.add_player("p1", "Alice", MockWebSocket())
room2.add_player("p2", "Bob", MockWebSocket())
assert rm.find_player_room("p1") is room1
assert rm.find_player_room("p2") is room2
# =============================================================================
# Room player management
# =============================================================================
class TestRoomPlayers:
def test_add_player_first_is_host(self):
room = Room(code="TEST")
ws = MockWebSocket()
rp = room.add_player("p1", "Alice", ws)
assert rp.is_host is True
def test_add_player_second_is_not_host(self):
room = Room(code="TEST")
room.add_player("p1", "Alice", MockWebSocket())
rp2 = room.add_player("p2", "Bob", MockWebSocket())
assert rp2.is_host is False
def test_remove_player(self):
room = Room(code="TEST")
room.add_player("p1", "Alice", MockWebSocket())
removed = room.remove_player("p1")
assert removed is not None
assert removed.id == "p1"
assert "p1" not in room.players
def test_remove_nonexistent_player(self):
room = Room(code="TEST")
result = room.remove_player("nobody")
assert result is None
def test_host_reassignment_on_remove(self):
room = Room(code="TEST")
room.add_player("p1", "Alice", MockWebSocket())
room.add_player("p2", "Bob", MockWebSocket())
room.remove_player("p1")
assert room.players["p2"].is_host is True
def test_get_player(self):
room = Room(code="TEST")
room.add_player("p1", "Alice", MockWebSocket())
assert room.get_player("p1") is not None
assert room.get_player("p1").name == "Alice"
assert room.get_player("nobody") is None
def test_is_empty(self):
room = Room(code="TEST")
assert room.is_empty() is True
room.add_player("p1", "Alice", MockWebSocket())
assert room.is_empty() is False
def test_player_list(self):
room = Room(code="TEST")
room.add_player("p1", "Alice", MockWebSocket())
room.add_player("p2", "Bob", MockWebSocket())
plist = room.player_list()
assert len(plist) == 2
assert plist[0]["name"] == "Alice"
assert plist[0]["is_host"] is True
assert plist[1]["is_cpu"] is False
def test_human_player_count(self):
room = Room(code="TEST")
room.add_player("p1", "Alice", MockWebSocket())
assert room.human_player_count() == 1
def test_auth_user_id_stored(self):
room = Room(code="TEST")
rp = room.add_player("p1", "Alice", MockWebSocket(), auth_user_id="auth_123")
assert rp.auth_user_id == "auth_123"
# =============================================================================
# CPU player management
# =============================================================================
class TestCPUPlayers:
def test_add_cpu_player(self):
room = Room(code="TEST")
room.add_player("host", "Host", MockWebSocket())
with patch("room.assign_profile") as mock_assign:
from ai import CPUProfile
mock_assign.return_value = CPUProfile(
name="TestBot", style="balanced",
pair_hope=0.5, aggression=0.5,
swap_threshold=4, unpredictability=0.1,
)
rp = room.add_cpu_player("cpu_1")
assert rp is not None
assert rp.is_cpu is True
assert rp.name == "TestBot"
def test_add_cpu_player_no_profile(self):
room = Room(code="TEST")
room.add_player("host", "Host", MockWebSocket())
with patch("room.assign_profile", return_value=None):
rp = room.add_cpu_player("cpu_1")
assert rp is None
def test_get_cpu_players(self):
room = Room(code="TEST")
room.add_player("host", "Host", MockWebSocket())
with patch("room.assign_profile") as mock_assign:
from ai import CPUProfile
mock_assign.return_value = CPUProfile(
name="Bot", style="balanced",
pair_hope=0.5, aggression=0.5,
swap_threshold=4, unpredictability=0.1,
)
room.add_cpu_player("cpu_1")
cpus = room.get_cpu_players()
assert len(cpus) == 1
assert cpus[0].is_cpu is True
def test_remove_cpu_releases_profile(self):
room = Room(code="TEST")
room.add_player("host", "Host", MockWebSocket())
with patch("room.assign_profile") as mock_assign:
from ai import CPUProfile
mock_assign.return_value = CPUProfile(
name="Bot", style="balanced",
pair_hope=0.5, aggression=0.5,
swap_threshold=4, unpredictability=0.1,
)
room.add_cpu_player("cpu_1")
with patch("room.release_profile") as mock_release:
room.remove_player("cpu_1")
mock_release.assert_called_once_with("Bot", "TEST")
# =============================================================================
# Broadcast / send_to
# =============================================================================
class TestMessaging:
@pytest.mark.asyncio
async def test_broadcast_to_all_humans(self):
room = Room(code="TEST")
ws1 = MockWebSocket()
ws2 = MockWebSocket()
room.add_player("p1", "Alice", ws1)
room.add_player("p2", "Bob", ws2)
await room.broadcast({"type": "test_msg"})
assert len(ws1.messages) == 1
assert len(ws2.messages) == 1
assert ws1.messages[0]["type"] == "test_msg"
@pytest.mark.asyncio
async def test_broadcast_excludes_player(self):
room = Room(code="TEST")
ws1 = MockWebSocket()
ws2 = MockWebSocket()
room.add_player("p1", "Alice", ws1)
room.add_player("p2", "Bob", ws2)
await room.broadcast({"type": "test_msg"}, exclude="p1")
assert len(ws1.messages) == 0
assert len(ws2.messages) == 1
@pytest.mark.asyncio
async def test_broadcast_skips_cpu(self):
room = Room(code="TEST")
ws1 = MockWebSocket()
room.add_player("p1", "Alice", ws1)
# Add a CPU player manually (no websocket)
room.players["cpu_1"] = RoomPlayer(
id="cpu_1", name="Bot", websocket=None, is_cpu=True
)
await room.broadcast({"type": "test_msg"})
assert len(ws1.messages) == 1
# CPU has no websocket, no error
@pytest.mark.asyncio
async def test_send_to_specific_player(self):
room = Room(code="TEST")
ws1 = MockWebSocket()
ws2 = MockWebSocket()
room.add_player("p1", "Alice", ws1)
room.add_player("p2", "Bob", ws2)
await room.send_to("p1", {"type": "private_msg"})
assert len(ws1.messages) == 1
assert len(ws2.messages) == 0
@pytest.mark.asyncio
async def test_send_to_nonexistent_player(self):
room = Room(code="TEST")
await room.send_to("nobody", {"type": "test"}) # Should not raise
@pytest.mark.asyncio
async def test_send_to_cpu_is_noop(self):
room = Room(code="TEST")
room.players["cpu_1"] = RoomPlayer(
id="cpu_1", name="Bot", websocket=None, is_cpu=True
)
await room.send_to("cpu_1", {"type": "test"}) # Should not raise