Three changes to fail fast instead of silently degrading: 1. GameService.create_game: Raise GameCreationError when energy card definition not found instead of logging warning and continuing. A deck with missing energy cards is fundamentally broken. 2. CardService.load_all: Collect all card file load failures and raise CardServiceLoadError at end with comprehensive error report. Prevents startup with partial card data that causes cryptic runtime errors. New exceptions: CardLoadError, CardServiceLoadError 3. GameStateManager.recover_active_games: Return RecoveryResult dataclass with recovered count, failed game IDs with error messages, and total. Enables proper monitoring and alerting for corrupted game state. Tests added for energy card error case. Existing tests updated for new RecoveryResult return type. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
704 lines
23 KiB
Python
704 lines
23 KiB
Python
"""Tests for GameStateManager service.
|
|
|
|
This module tests the Redis-primary, Postgres-backup game state management:
|
|
- Cache operations (save/load/delete from Redis)
|
|
- Database operations (persist/load/delete from Postgres)
|
|
- High-level operations (load with fallback, recovery)
|
|
|
|
Prerequisites:
|
|
docker compose up -d # Postgres on 5433, Redis on 6380
|
|
"""
|
|
|
|
import json
|
|
from typing import Any
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.enums import TurnPhase
|
|
from app.core.models.game_state import GameState
|
|
from app.db.models import GameType
|
|
from app.db.redis import RedisHelper
|
|
from app.services.game_state_manager import (
|
|
GAME_KEY_PREFIX,
|
|
GameStateManager,
|
|
)
|
|
from tests.factories import UserFactory
|
|
from tests.services.conftest import create_test_game_state
|
|
|
|
# =============================================================================
|
|
# Mock Redis Helper for Unit Tests
|
|
# =============================================================================
|
|
|
|
|
|
class MockRedisHelper(RedisHelper):
|
|
"""In-memory mock Redis for unit testing without real Redis."""
|
|
|
|
def __init__(self) -> None:
|
|
self.store: dict[str, str] = {}
|
|
self.ttls: dict[str, int | None] = {}
|
|
|
|
async def get_json(self, key: str) -> dict[str, Any] | None:
|
|
"""Get JSON from mock store."""
|
|
value = self.store.get(key)
|
|
if value is None:
|
|
return None
|
|
return json.loads(value)
|
|
|
|
async def set_json(
|
|
self,
|
|
key: str,
|
|
value: dict[str, Any],
|
|
expire_seconds: int | None = None,
|
|
) -> None:
|
|
"""Store JSON in mock store."""
|
|
self.store[key] = json.dumps(value, default=str)
|
|
self.ttls[key] = expire_seconds
|
|
|
|
async def delete(self, key: str) -> bool:
|
|
"""Delete from mock store."""
|
|
if key in self.store:
|
|
del self.store[key]
|
|
self.ttls.pop(key, None)
|
|
return True
|
|
return False
|
|
|
|
async def exists(self, key: str) -> bool:
|
|
"""Check if key exists in mock store."""
|
|
return key in self.store
|
|
|
|
|
|
# =============================================================================
|
|
# Cache Operation Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestCacheOperations:
|
|
"""Tests for Redis cache operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_save_to_cache(self, redis_client: Any) -> None:
|
|
"""Test saving game state to Redis cache.
|
|
|
|
Verifies that GameState is serialized to JSON and stored with
|
|
the correct key format.
|
|
"""
|
|
# Create a mock helper that uses our test redis client
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
game = create_test_game_state()
|
|
|
|
await manager.save_to_cache(game)
|
|
|
|
# Verify key exists
|
|
key = f"{GAME_KEY_PREFIX}{game.game_id}"
|
|
assert key in helper.store
|
|
|
|
# Verify data is valid JSON that can be deserialized
|
|
data = json.loads(helper.store[key])
|
|
assert data["game_id"] == game.game_id
|
|
assert data["turn_number"] == game.turn_number
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_from_cache_hit(self) -> None:
|
|
"""Test loading game state from cache when it exists.
|
|
|
|
Verifies cache hit returns the correct GameState.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
game = create_test_game_state(turn_number=5)
|
|
|
|
# Save first
|
|
await manager.save_to_cache(game)
|
|
|
|
# Load and verify
|
|
loaded = await manager.load_from_cache(game.game_id)
|
|
assert loaded is not None
|
|
assert loaded.game_id == game.game_id
|
|
assert loaded.turn_number == 5
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_from_cache_miss(self) -> None:
|
|
"""Test loading game state from cache when it doesn't exist.
|
|
|
|
Verifies cache miss returns None.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
loaded = await manager.load_from_cache("nonexistent-game")
|
|
assert loaded is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_from_cache(self) -> None:
|
|
"""Test deleting game state from cache.
|
|
|
|
Verifies game is removed and subsequent loads return None.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
game = create_test_game_state()
|
|
|
|
await manager.save_to_cache(game)
|
|
assert await manager.cache_exists(game.game_id)
|
|
|
|
deleted = await manager.delete_from_cache(game.game_id)
|
|
assert deleted is True
|
|
assert not await manager.cache_exists(game.game_id)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_from_cache_not_found(self) -> None:
|
|
"""Test deleting game that doesn't exist in cache.
|
|
|
|
Verifies returns False when game not found.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
deleted = await manager.delete_from_cache("nonexistent-game")
|
|
assert deleted is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cache_exists(self) -> None:
|
|
"""Test checking if game exists in cache.
|
|
|
|
Verifies exists returns True for cached games, False otherwise.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
game = create_test_game_state()
|
|
|
|
assert not await manager.cache_exists(game.game_id)
|
|
|
|
await manager.save_to_cache(game)
|
|
assert await manager.cache_exists(game.game_id)
|
|
|
|
|
|
# =============================================================================
|
|
# Database Operation Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestDatabaseOperations:
|
|
"""Tests for Postgres database operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_persist_to_db_creates_new(self, db_session: AsyncSession) -> None:
|
|
"""Test persisting game state creates new ActiveGame record.
|
|
|
|
Verifies a new game is properly stored in Postgres.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
# Create user for FK
|
|
user = await UserFactory.create(db_session)
|
|
game = create_test_game_state()
|
|
|
|
# Persist to database
|
|
active_game = await manager.persist_to_db(
|
|
game,
|
|
game_type=GameType.CAMPAIGN,
|
|
player1_id=user.id,
|
|
npc_id="test_npc",
|
|
session=db_session,
|
|
)
|
|
|
|
assert active_game.id == game.game_id
|
|
assert active_game.game_type == GameType.CAMPAIGN
|
|
assert active_game.player1_id == user.id
|
|
assert active_game.npc_id == "test_npc"
|
|
assert active_game.turn_number == game.turn_number
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_persist_to_db_updates_existing(self, db_session: AsyncSession) -> None:
|
|
"""Test persisting game state updates existing ActiveGame record.
|
|
|
|
Verifies an existing game is updated with new state.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
user = await UserFactory.create(db_session)
|
|
game = create_test_game_state(turn_number=1)
|
|
|
|
# Create initial record
|
|
await manager.persist_to_db(
|
|
game,
|
|
game_type=GameType.CAMPAIGN,
|
|
player1_id=user.id,
|
|
session=db_session,
|
|
)
|
|
|
|
# Update game state
|
|
game = GameState.model_validate(
|
|
{**game.model_dump(), "turn_number": 10, "phase": TurnPhase.ATTACK.value}
|
|
)
|
|
|
|
# Persist again
|
|
active_game = await manager.persist_to_db(
|
|
game,
|
|
game_type=GameType.CAMPAIGN,
|
|
player1_id=user.id,
|
|
session=db_session,
|
|
)
|
|
|
|
# Verify update
|
|
assert active_game.turn_number == 10
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_from_db_found(self, db_session: AsyncSession) -> None:
|
|
"""Test loading game state from database when it exists.
|
|
|
|
Verifies game state is correctly deserialized from Postgres.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
user = await UserFactory.create(db_session)
|
|
game = create_test_game_state(turn_number=7)
|
|
|
|
# Persist first
|
|
await manager.persist_to_db(
|
|
game,
|
|
game_type=GameType.CAMPAIGN,
|
|
player1_id=user.id,
|
|
session=db_session,
|
|
)
|
|
|
|
# Load and verify
|
|
loaded = await manager.load_from_db(game.game_id, session=db_session)
|
|
assert loaded is not None
|
|
assert loaded.game_id == game.game_id
|
|
assert loaded.turn_number == 7
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_from_db_not_found(self, db_session: AsyncSession) -> None:
|
|
"""Test loading game state from database when it doesn't exist.
|
|
|
|
Verifies returns None for missing games.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
loaded = await manager.load_from_db(str(uuid4()), session=db_session)
|
|
assert loaded is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_from_db(self, db_session: AsyncSession) -> None:
|
|
"""Test deleting game from database.
|
|
|
|
Verifies game is removed from ActiveGame table.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
user = await UserFactory.create(db_session)
|
|
game = create_test_game_state()
|
|
|
|
# Create game
|
|
await manager.persist_to_db(
|
|
game,
|
|
game_type=GameType.CAMPAIGN,
|
|
player1_id=user.id,
|
|
session=db_session,
|
|
)
|
|
|
|
# Delete it
|
|
deleted = await manager.delete_from_db(game.game_id, session=db_session)
|
|
assert deleted is True
|
|
|
|
# Verify gone
|
|
loaded = await manager.load_from_db(game.game_id, session=db_session)
|
|
assert loaded is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_from_db_not_found(self, db_session: AsyncSession) -> None:
|
|
"""Test deleting game that doesn't exist in database.
|
|
|
|
Verifies returns False for missing games.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
deleted = await manager.delete_from_db(str(uuid4()), session=db_session)
|
|
assert deleted is False
|
|
|
|
|
|
# =============================================================================
|
|
# High-Level Operation Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestHighLevelOperations:
|
|
"""Tests for high-level game state operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_state_cache_hit(self) -> None:
|
|
"""Test load_state returns cached data when available.
|
|
|
|
Verifies the fast path uses Redis when game is cached.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
game = create_test_game_state()
|
|
|
|
# Cache the game
|
|
await manager.save_to_cache(game)
|
|
|
|
# Load should use cache (fast path)
|
|
loaded = await manager.load_state(game.game_id)
|
|
assert loaded is not None
|
|
assert loaded.game_id == game.game_id
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_state_cache_miss_db_hit(self, db_session: AsyncSession) -> None:
|
|
"""Test load_state falls back to database on cache miss.
|
|
|
|
Verifies database fallback and cache repopulation.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
user = await UserFactory.create(db_session)
|
|
game = create_test_game_state()
|
|
|
|
# Only in database, not in cache
|
|
await manager.persist_to_db(
|
|
game,
|
|
game_type=GameType.CAMPAIGN,
|
|
player1_id=user.id,
|
|
session=db_session,
|
|
)
|
|
|
|
# Load should fallback to DB (pass session for test)
|
|
loaded = await manager.load_state(game.game_id, session=db_session)
|
|
assert loaded is not None
|
|
assert loaded.game_id == game.game_id
|
|
|
|
# Should now be in cache
|
|
assert await manager.cache_exists(game.game_id)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_state_not_found(self, db_session: AsyncSession) -> None:
|
|
"""Test load_state returns None when game doesn't exist anywhere.
|
|
|
|
Verifies proper handling of missing games.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
loaded = await manager.load_state(str(uuid4()), session=db_session)
|
|
assert loaded is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_game(self, db_session: AsyncSession) -> None:
|
|
"""Test delete_game removes from both cache and database.
|
|
|
|
Verifies complete cleanup of game state.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
user = await UserFactory.create(db_session)
|
|
game = create_test_game_state()
|
|
|
|
# Create in both cache and DB
|
|
await manager.save_to_cache(game)
|
|
await manager.persist_to_db(
|
|
game,
|
|
game_type=GameType.CAMPAIGN,
|
|
player1_id=user.id,
|
|
session=db_session,
|
|
)
|
|
|
|
# Delete completely (pass session)
|
|
deleted = await manager.delete_game(game.game_id, session=db_session)
|
|
assert deleted is True
|
|
|
|
# Verify gone from both
|
|
assert not await manager.cache_exists(game.game_id)
|
|
loaded = await manager.load_from_db(game.game_id, session=db_session)
|
|
assert loaded is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_recover_active_games(self, db_session: AsyncSession) -> None:
|
|
"""Test recovering active games from database to cache on startup.
|
|
|
|
Verifies all games are loaded from Postgres and cached in Redis.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
user = await UserFactory.create(db_session)
|
|
|
|
# Create multiple games in DB only
|
|
game1 = create_test_game_state()
|
|
game2 = create_test_game_state()
|
|
|
|
await manager.persist_to_db(
|
|
game1,
|
|
game_type=GameType.CAMPAIGN,
|
|
player1_id=user.id,
|
|
session=db_session,
|
|
)
|
|
await manager.persist_to_db(
|
|
game2,
|
|
game_type=GameType.FREEPLAY,
|
|
player1_id=user.id,
|
|
session=db_session,
|
|
)
|
|
|
|
# Clear cache
|
|
helper.store.clear()
|
|
|
|
# Recover (pass session)
|
|
result = await manager.recover_active_games(session=db_session)
|
|
assert result.recovered == 2
|
|
assert result.total == 2
|
|
assert len(result.failed) == 0
|
|
|
|
# Both should be in cache now
|
|
assert await manager.cache_exists(game1.game_id)
|
|
assert await manager.cache_exists(game2.game_id)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_player_active_games(self, db_session: AsyncSession) -> None:
|
|
"""Test getting all active games for a player.
|
|
|
|
Verifies correct games are returned for a specific player.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
user1 = await UserFactory.create(db_session)
|
|
user2 = await UserFactory.create(db_session)
|
|
|
|
# Game where user1 is player1
|
|
game1 = create_test_game_state()
|
|
await manager.persist_to_db(
|
|
game1,
|
|
game_type=GameType.CAMPAIGN,
|
|
player1_id=user1.id,
|
|
session=db_session,
|
|
)
|
|
|
|
# Game where user1 is player2
|
|
game2 = create_test_game_state()
|
|
await manager.persist_to_db(
|
|
game2,
|
|
game_type=GameType.FREEPLAY,
|
|
player1_id=user2.id,
|
|
player2_id=user1.id,
|
|
session=db_session,
|
|
)
|
|
|
|
# Game for different user
|
|
game3 = create_test_game_state()
|
|
await manager.persist_to_db(
|
|
game3,
|
|
game_type=GameType.CAMPAIGN,
|
|
player1_id=user2.id,
|
|
session=db_session,
|
|
)
|
|
|
|
# Get user1's games
|
|
games = await manager.get_player_active_games(user1.id, session=db_session)
|
|
game_ids = {str(g.id) for g in games}
|
|
|
|
assert game1.game_id in game_ids
|
|
assert game2.game_id in game_ids
|
|
assert game3.game_id not in game_ids
|
|
|
|
|
|
# =============================================================================
|
|
# Integration Tests with Real Redis
|
|
# =============================================================================
|
|
|
|
|
|
class TestRealRedisIntegration:
|
|
"""Integration tests using real Redis (from testcontainers)."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_save_and_load_with_real_redis(self, redis_url: str) -> None:
|
|
"""Test full save/load cycle with real Redis client.
|
|
|
|
Verifies the complete flow works with actual Redis.
|
|
Creates Redis client inside test to avoid event loop issues.
|
|
"""
|
|
import redis.asyncio as aioredis
|
|
|
|
# Create client inside test (same event loop)
|
|
client = aioredis.from_url(redis_url, decode_responses=True)
|
|
|
|
try:
|
|
# Clear test database
|
|
await client.flushdb()
|
|
|
|
# Create a helper that uses the test client
|
|
class TestRedisHelper(RedisHelper):
|
|
def __init__(self, redis_client: Any) -> None:
|
|
self._client = redis_client
|
|
|
|
async def get_json(self, key: str) -> dict | None:
|
|
value = await self._client.get(key)
|
|
if value is None:
|
|
return None
|
|
return json.loads(value)
|
|
|
|
async def set_json(
|
|
self, key: str, value: dict, expire_seconds: int | None = None
|
|
) -> None:
|
|
json_str = json.dumps(value, default=str)
|
|
if expire_seconds:
|
|
await self._client.setex(key, expire_seconds, json_str)
|
|
else:
|
|
await self._client.set(key, json_str)
|
|
|
|
async def delete(self, key: str) -> bool:
|
|
result = await self._client.delete(key)
|
|
return result > 0
|
|
|
|
async def exists(self, key: str) -> bool:
|
|
result = await self._client.exists(key)
|
|
return result > 0
|
|
|
|
helper = TestRedisHelper(client)
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
game = create_test_game_state(turn_number=42)
|
|
|
|
# Save to real Redis
|
|
await manager.save_to_cache(game)
|
|
|
|
# Load from real Redis
|
|
loaded = await manager.load_from_cache(game.game_id)
|
|
|
|
assert loaded is not None
|
|
assert loaded.game_id == game.game_id
|
|
assert loaded.turn_number == 42
|
|
|
|
# Clean up
|
|
await manager.delete_from_cache(game.game_id)
|
|
assert not await manager.cache_exists(game.game_id)
|
|
|
|
finally:
|
|
await client.flushdb()
|
|
await client.aclose()
|
|
|
|
|
|
# =============================================================================
|
|
# Additional Coverage Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestAdditionalCoverage:
|
|
"""Tests to fill coverage gaps identified in review."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_active_game_count(self, db_session: AsyncSession) -> None:
|
|
"""Test counting active games in database.
|
|
|
|
Verifies the monitoring/admin dashboard helper works correctly.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
user = await UserFactory.create(db_session)
|
|
|
|
# Initially zero
|
|
count = await manager.get_active_game_count(session=db_session)
|
|
assert count == 0
|
|
|
|
# Create some games
|
|
game1 = create_test_game_state()
|
|
game2 = create_test_game_state()
|
|
await manager.persist_to_db(
|
|
game1,
|
|
game_type=GameType.CAMPAIGN,
|
|
player1_id=user.id,
|
|
session=db_session,
|
|
)
|
|
await manager.persist_to_db(
|
|
game2,
|
|
game_type=GameType.FREEPLAY,
|
|
player1_id=user.id,
|
|
session=db_session,
|
|
)
|
|
|
|
# Now should be 2
|
|
count = await manager.get_active_game_count(session=db_session)
|
|
assert count == 2
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_recover_active_games_with_invalid_state(self, db_session: AsyncSession) -> None:
|
|
"""Test recovery handles corrupted game state gracefully.
|
|
|
|
Verifies the exception handler in recover_active_games() logs errors
|
|
but continues recovering other games.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
user = await UserFactory.create(db_session)
|
|
|
|
# Create a valid game
|
|
valid_game = create_test_game_state()
|
|
await manager.persist_to_db(
|
|
valid_game,
|
|
game_type=GameType.CAMPAIGN,
|
|
player1_id=user.id,
|
|
session=db_session,
|
|
)
|
|
|
|
# Manually insert a game with invalid/corrupted state
|
|
from sqlalchemy import text
|
|
|
|
invalid_game_id = str(uuid4())
|
|
await db_session.execute(
|
|
text("""
|
|
INSERT INTO active_games
|
|
(id, game_type, player1_id, rules_config, game_state, turn_number,
|
|
started_at, last_action_at, created_at, updated_at)
|
|
VALUES
|
|
(:id, 'CAMPAIGN', :player_id, '{}', '{"invalid": "not a valid game state"}',
|
|
1, NOW(), NOW(), NOW(), NOW())
|
|
"""),
|
|
{"id": invalid_game_id, "player_id": str(user.id)},
|
|
)
|
|
await db_session.flush()
|
|
|
|
# Clear cache
|
|
helper.store.clear()
|
|
|
|
# Recovery should handle the error and still recover the valid game
|
|
result = await manager.recover_active_games(session=db_session)
|
|
|
|
# Only the valid game should be recovered (invalid one fails validation)
|
|
assert result.recovered == 1
|
|
assert result.total == 2
|
|
assert len(result.failed) == 1
|
|
# Verify the failed game ID is reported
|
|
failed_ids = [gid for gid, _ in result.failed]
|
|
assert invalid_game_id in failed_ids
|
|
assert await manager.cache_exists(valid_game.game_id)
|
|
assert not await manager.cache_exists(invalid_game_id)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_game_not_found_anywhere(self, db_session: AsyncSession) -> None:
|
|
"""Test delete_game when game doesn't exist in cache or DB.
|
|
|
|
Verifies the warning log path and returns False.
|
|
"""
|
|
helper = MockRedisHelper()
|
|
manager = GameStateManager(redis=helper)
|
|
|
|
# Try to delete a game that doesn't exist anywhere
|
|
nonexistent_id = str(uuid4())
|
|
deleted = await manager.delete_game(nonexistent_id, session=db_session)
|
|
|
|
# Should return False (nothing was deleted)
|
|
assert deleted is False
|