- Create tests/conftest.py with testcontainers for Postgres and Redis - Auto-detect Docker Desktop socket and disable Ryuk for compatibility - Update tests/db/conftest.py and tests/services/conftest.py to use shared fixtures - Fix test_resolve_effect_logs_exceptions: logger was disabled by pytest - Fix test_save_and_load_with_real_redis: use redis_url fixture - Minor lint fix in engine_validation.py Tests now auto-start containers on run - no need for `docker compose up` All 1199 tests passing. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
697 lines
23 KiB
Python
697 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)
|
|
count = await manager.recover_active_games(session=db_session)
|
|
assert count == 2
|
|
|
|
# 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
|
|
count = await manager.recover_active_games(session=db_session)
|
|
|
|
# Only the valid game should be recovered (invalid one fails validation)
|
|
assert count == 1
|
|
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
|