golfgame/server/test_handlers.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

294 lines
8.2 KiB
Python

"""
Test suite for WebSocket message handlers.
Tests handler basic flows and validation using mock WebSocket/Room.
Run with: pytest test_handlers.py -v
"""
import asyncio
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from game import Game, GamePhase, GameOptions, Player
from room import Room, RoomPlayer, RoomManager
from handlers import (
ConnectionContext,
handle_create_room,
handle_join_room,
handle_draw,
handle_swap,
handle_discard,
)
# =============================================================================
# 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)
def last_message(self) -> dict:
return self.messages[-1] if self.messages else {}
def messages_of_type(self, msg_type: str) -> list[dict]:
return [m for m in self.messages if m.get("type") == msg_type]
def make_ctx(websocket=None, player_id="test_player", room=None):
"""Create a ConnectionContext with sensible defaults."""
ws = websocket or MockWebSocket()
return ConnectionContext(
websocket=ws,
connection_id="conn_123",
player_id=player_id,
auth_user_id=None,
authenticated_user=None,
current_room=room,
)
def make_room_manager():
"""Create a RoomManager for testing."""
return RoomManager()
def make_room_with_game(num_players=2):
"""Create a Room with players and a game in PLAYING phase."""
room = Room(code="TEST")
for i in range(num_players):
ws = MockWebSocket()
room.add_player(f"p{i}", f"Player {i}", ws)
room.game.start_game(num_decks=1, num_rounds=1, options=GameOptions(initial_flips=0))
# Skip initial flip phase
for p in room.game.players:
room.game.flip_initial_cards(p.id, [0, 1])
return room
# =============================================================================
# Lobby handlers
# =============================================================================
class TestHandleCreateRoom:
@pytest.mark.asyncio
async def test_creates_room(self):
ws = MockWebSocket()
ctx = make_ctx(websocket=ws)
rm = make_room_manager()
await handle_create_room(
{"player_name": "Alice"},
ctx,
room_manager=rm,
count_user_games=lambda uid: 0,
max_concurrent=5,
)
assert ctx.current_room is not None
assert len(rm.rooms) == 1
assert ws.messages_of_type("room_created")
@pytest.mark.asyncio
async def test_max_concurrent_rejects(self):
ws = MockWebSocket()
ctx = make_ctx(websocket=ws)
ctx.auth_user_id = "user1"
rm = make_room_manager()
await handle_create_room(
{"player_name": "Alice"},
ctx,
room_manager=rm,
count_user_games=lambda uid: 5,
max_concurrent=5,
)
assert ctx.current_room is None
assert ws.messages_of_type("error")
class TestHandleJoinRoom:
@pytest.mark.asyncio
async def test_join_existing_room(self):
rm = make_room_manager()
room = rm.create_room()
host_ws = MockWebSocket()
room.add_player("host", "Host", host_ws)
ws = MockWebSocket()
ctx = make_ctx(websocket=ws, player_id="joiner")
await handle_join_room(
{"room_code": room.code, "player_name": "Bob"},
ctx,
room_manager=rm,
count_user_games=lambda uid: 0,
max_concurrent=5,
)
assert ctx.current_room is room
assert ws.messages_of_type("room_joined")
assert len(room.players) == 2
@pytest.mark.asyncio
async def test_join_nonexistent_room(self):
rm = make_room_manager()
ws = MockWebSocket()
ctx = make_ctx(websocket=ws)
await handle_join_room(
{"room_code": "ZZZZ", "player_name": "Bob"},
ctx,
room_manager=rm,
count_user_games=lambda uid: 0,
max_concurrent=5,
)
assert ctx.current_room is None
assert ws.messages_of_type("error")
assert "not found" in ws.last_message().get("message", "").lower()
@pytest.mark.asyncio
async def test_join_full_room(self):
rm = make_room_manager()
room = rm.create_room()
for i in range(6):
room.add_player(f"p{i}", f"Player {i}", MockWebSocket())
ws = MockWebSocket()
ctx = make_ctx(websocket=ws, player_id="extra")
await handle_join_room(
{"room_code": room.code, "player_name": "Extra"},
ctx,
room_manager=rm,
count_user_games=lambda uid: 0,
max_concurrent=5,
)
assert ws.messages_of_type("error")
assert "full" in ws.last_message().get("message", "").lower()
@pytest.mark.asyncio
async def test_join_in_progress_game(self):
rm = make_room_manager()
room = rm.create_room()
room.add_player("host", "Host", MockWebSocket())
room.add_player("p2", "Player 2", MockWebSocket())
room.game.start_game(1, 1, GameOptions(initial_flips=0))
ws = MockWebSocket()
ctx = make_ctx(websocket=ws, player_id="late")
await handle_join_room(
{"room_code": room.code, "player_name": "Late"},
ctx,
room_manager=rm,
count_user_games=lambda uid: 0,
max_concurrent=5,
)
assert ws.messages_of_type("error")
assert "in progress" in ws.last_message().get("message", "").lower()
# =============================================================================
# Turn action handlers
# =============================================================================
class TestHandleDraw:
@pytest.mark.asyncio
async def test_draw_from_deck(self):
room = make_room_with_game()
current_pid = room.game.players[room.game.current_player_index].id
ws = room.players[current_pid].websocket
ctx = make_ctx(websocket=ws, player_id=current_pid, room=room)
broadcast = AsyncMock()
await handle_draw(
{"source": "deck"},
ctx,
broadcast_game_state=broadcast,
)
assert ws.messages_of_type("card_drawn")
broadcast.assert_called_once()
@pytest.mark.asyncio
async def test_draw_no_room(self):
ws = MockWebSocket()
ctx = make_ctx(websocket=ws, room=None)
broadcast = AsyncMock()
await handle_draw(
{"source": "deck"},
ctx,
broadcast_game_state=broadcast,
)
assert len(ws.messages) == 0
broadcast.assert_not_called()
class TestHandleSwap:
@pytest.mark.asyncio
async def test_swap_card(self):
room = make_room_with_game()
current_pid = room.game.players[room.game.current_player_index].id
ws = room.players[current_pid].websocket
# Draw a card first
room.game.draw_card(current_pid, "deck")
ctx = make_ctx(websocket=ws, player_id=current_pid, room=room)
broadcast = AsyncMock()
check_cpu = AsyncMock()
await handle_swap(
{"position": 0},
ctx,
broadcast_game_state=broadcast,
check_and_run_cpu_turn=check_cpu,
)
broadcast.assert_called_once()
class TestHandleDiscard:
@pytest.mark.asyncio
async def test_discard_drawn_card(self):
room = make_room_with_game()
current_pid = room.game.players[room.game.current_player_index].id
ws = room.players[current_pid].websocket
room.game.draw_card(current_pid, "deck")
ctx = make_ctx(websocket=ws, player_id=current_pid, room=room)
broadcast = AsyncMock()
check_cpu = AsyncMock()
await handle_discard(
{},
ctx,
broadcast_game_state=broadcast,
check_and_run_cpu_turn=check_cpu,
)
broadcast.assert_called_once()