567 lines
19 KiB
Python
567 lines
19 KiB
Python
"""
|
|
Tests for the SessionService.
|
|
|
|
Tests cover:
|
|
- Solo session creation
|
|
- Session retrieval and listing
|
|
- Conversation history management
|
|
- Game state tracking (location, quests, events)
|
|
- Session validation and limits
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
from datetime import datetime
|
|
|
|
from app.services.session_service import (
|
|
SessionService,
|
|
SessionNotFound,
|
|
SessionLimitExceeded,
|
|
SessionValidationError,
|
|
MAX_ACTIVE_SESSIONS,
|
|
)
|
|
from app.models.session import GameSession, GameState, ConversationEntry
|
|
from app.models.enums import SessionStatus, SessionType, LocationType
|
|
from app.models.character import Character
|
|
from app.models.skills import PlayerClass
|
|
from app.models.origins import Origin
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_db():
|
|
"""Create mock database service."""
|
|
with patch('app.services.session_service.get_database_service') as mock:
|
|
db = Mock()
|
|
mock.return_value = db
|
|
yield db
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_appwrite():
|
|
"""Create mock Appwrite service."""
|
|
with patch('app.services.session_service.AppwriteService') as mock:
|
|
service = Mock()
|
|
mock.return_value = service
|
|
yield service
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_character_service():
|
|
"""Create mock character service."""
|
|
with patch('app.services.session_service.get_character_service') as mock:
|
|
service = Mock()
|
|
mock.return_value = service
|
|
yield service
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_character():
|
|
"""Create a sample character for testing."""
|
|
return Character(
|
|
character_id="char_123",
|
|
user_id="user_456",
|
|
name="Test Hero",
|
|
player_class=Mock(spec=PlayerClass),
|
|
origin=Mock(spec=Origin),
|
|
level=5,
|
|
experience=1000,
|
|
base_stats={"strength": 10},
|
|
unlocked_skills=[],
|
|
inventory=[],
|
|
equipped={},
|
|
gold=100,
|
|
active_quests=[],
|
|
discovered_locations=[],
|
|
current_location="Town"
|
|
)
|
|
|
|
|
|
class TestSessionServiceCreation:
|
|
"""Tests for session creation."""
|
|
|
|
def test_create_solo_session_success(self, mock_db, mock_appwrite, mock_character_service, sample_character):
|
|
"""Test successful solo session creation."""
|
|
mock_character_service.get_character.return_value = sample_character
|
|
mock_db.count_documents.return_value = 0
|
|
mock_db.create_document.return_value = None
|
|
|
|
service = SessionService()
|
|
session = service.create_solo_session(
|
|
user_id="user_456",
|
|
character_id="char_123"
|
|
)
|
|
|
|
assert session.session_type == SessionType.SOLO
|
|
assert session.solo_character_id == "char_123"
|
|
assert session.user_id == "user_456"
|
|
assert session.turn_number == 0
|
|
assert session.status == SessionStatus.ACTIVE
|
|
assert session.game_state.current_location == "Crossroads Village"
|
|
assert session.game_state.location_type == LocationType.TOWN
|
|
|
|
mock_db.create_document.assert_called_once()
|
|
|
|
def test_create_solo_session_character_not_found(self, mock_db, mock_appwrite, mock_character_service):
|
|
"""Test session creation fails when character not found."""
|
|
from app.services.character_service import CharacterNotFound
|
|
mock_character_service.get_character.side_effect = CharacterNotFound("Not found")
|
|
|
|
service = SessionService()
|
|
with pytest.raises(CharacterNotFound):
|
|
service.create_solo_session(
|
|
user_id="user_456",
|
|
character_id="char_invalid"
|
|
)
|
|
|
|
def test_create_solo_session_limit_exceeded(self, mock_db, mock_appwrite, mock_character_service, sample_character):
|
|
"""Test session creation fails when limit exceeded."""
|
|
mock_character_service.get_character.return_value = sample_character
|
|
mock_db.count_documents.return_value = MAX_ACTIVE_SESSIONS
|
|
|
|
service = SessionService()
|
|
with pytest.raises(SessionLimitExceeded):
|
|
service.create_solo_session(
|
|
user_id="user_456",
|
|
character_id="char_123"
|
|
)
|
|
|
|
def test_create_solo_session_custom_location(self, mock_db, mock_appwrite, mock_character_service, sample_character):
|
|
"""Test session creation with custom starting location."""
|
|
mock_character_service.get_character.return_value = sample_character
|
|
mock_db.count_documents.return_value = 0
|
|
|
|
service = SessionService()
|
|
session = service.create_solo_session(
|
|
user_id="user_456",
|
|
character_id="char_123",
|
|
starting_location="Dark Forest",
|
|
starting_location_type=LocationType.WILDERNESS
|
|
)
|
|
|
|
assert session.game_state.current_location == "Dark Forest"
|
|
assert session.game_state.location_type == LocationType.WILDERNESS
|
|
assert "Dark Forest" in session.game_state.discovered_locations
|
|
|
|
|
|
class TestSessionServiceRetrieval:
|
|
"""Tests for session retrieval."""
|
|
|
|
def test_get_session_success(self, mock_db, mock_appwrite, mock_character_service):
|
|
"""Test successful session retrieval."""
|
|
session_data = {
|
|
"session_id": "sess_123",
|
|
"session_type": "solo",
|
|
"solo_character_id": "char_456",
|
|
"user_id": "user_789",
|
|
"party_member_ids": [],
|
|
"turn_number": 5,
|
|
"status": "active",
|
|
"game_state": {
|
|
"current_location": "Town",
|
|
"location_type": "town"
|
|
},
|
|
"conversation_history": [],
|
|
"config": {},
|
|
"turn_order": [],
|
|
"current_turn": 0
|
|
}
|
|
|
|
mock_document = Mock()
|
|
mock_document.data = {
|
|
'userId': 'user_789',
|
|
'sessionData': __import__('json').dumps(session_data)
|
|
}
|
|
mock_db.get_document.return_value = mock_document
|
|
|
|
service = SessionService()
|
|
session = service.get_session("sess_123", "user_789")
|
|
|
|
assert session.session_id == "sess_123"
|
|
assert session.user_id == "user_789"
|
|
assert session.turn_number == 5
|
|
|
|
def test_get_session_not_found(self, mock_db, mock_appwrite, mock_character_service):
|
|
"""Test session retrieval when not found."""
|
|
mock_db.get_document.return_value = None
|
|
|
|
service = SessionService()
|
|
with pytest.raises(SessionNotFound):
|
|
service.get_session("sess_invalid")
|
|
|
|
def test_get_session_wrong_user(self, mock_db, mock_appwrite, mock_character_service):
|
|
"""Test session retrieval with wrong user ID."""
|
|
mock_document = Mock()
|
|
mock_document.data = {
|
|
'userId': 'user_other',
|
|
'sessionData': '{}'
|
|
}
|
|
mock_db.get_document.return_value = mock_document
|
|
|
|
service = SessionService()
|
|
with pytest.raises(SessionNotFound):
|
|
service.get_session("sess_123", "user_wrong")
|
|
|
|
def test_get_user_sessions(self, mock_db, mock_appwrite, mock_character_service):
|
|
"""Test getting all sessions for a user."""
|
|
session_data = {
|
|
"session_id": "sess_1",
|
|
"session_type": "solo",
|
|
"user_id": "user_123",
|
|
"status": "active",
|
|
"turn_number": 0,
|
|
"game_state": {},
|
|
"conversation_history": [],
|
|
"config": {},
|
|
"turn_order": [],
|
|
"current_turn": 0,
|
|
"party_member_ids": []
|
|
}
|
|
|
|
mock_doc = Mock()
|
|
mock_doc.data = {'sessionData': __import__('json').dumps(session_data)}
|
|
mock_doc.id = "sess_1"
|
|
mock_db.list_rows.return_value = [mock_doc]
|
|
|
|
service = SessionService()
|
|
sessions = service.get_user_sessions("user_123")
|
|
|
|
assert len(sessions) == 1
|
|
assert sessions[0].session_id == "sess_1"
|
|
|
|
|
|
class TestConversationHistory:
|
|
"""Tests for conversation history management."""
|
|
|
|
def test_add_conversation_entry(self, mock_db, mock_appwrite, mock_character_service):
|
|
"""Test adding a conversation entry."""
|
|
session_data = {
|
|
"session_id": "sess_123",
|
|
"session_type": "solo",
|
|
"user_id": "user_456",
|
|
"turn_number": 0,
|
|
"status": "active",
|
|
"game_state": {"current_location": "Town", "location_type": "town"},
|
|
"conversation_history": [],
|
|
"config": {},
|
|
"turn_order": [],
|
|
"current_turn": 0,
|
|
"party_member_ids": []
|
|
}
|
|
|
|
mock_document = Mock()
|
|
mock_document.data = {
|
|
'userId': 'user_456',
|
|
'sessionData': __import__('json').dumps(session_data)
|
|
}
|
|
mock_db.get_document.return_value = mock_document
|
|
|
|
service = SessionService()
|
|
updated = service.add_conversation_entry(
|
|
session_id="sess_123",
|
|
character_id="char_789",
|
|
character_name="Hero",
|
|
action="I explore the area",
|
|
dm_response="You find a hidden path..."
|
|
)
|
|
|
|
assert updated.turn_number == 1
|
|
assert len(updated.conversation_history) == 1
|
|
assert updated.conversation_history[0].action == "I explore the area"
|
|
assert updated.conversation_history[0].dm_response == "You find a hidden path..."
|
|
|
|
mock_db.update_document.assert_called_once()
|
|
|
|
def test_add_conversation_entry_with_quest(self, mock_db, mock_appwrite, mock_character_service):
|
|
"""Test adding conversation entry with quest offering."""
|
|
session_data = {
|
|
"session_id": "sess_123",
|
|
"session_type": "solo",
|
|
"user_id": "user_456",
|
|
"turn_number": 5,
|
|
"status": "active",
|
|
"game_state": {},
|
|
"conversation_history": [],
|
|
"config": {},
|
|
"turn_order": [],
|
|
"current_turn": 0,
|
|
"party_member_ids": []
|
|
}
|
|
|
|
mock_document = Mock()
|
|
mock_document.data = {
|
|
'userId': 'user_456',
|
|
'sessionData': __import__('json').dumps(session_data)
|
|
}
|
|
mock_db.get_document.return_value = mock_document
|
|
|
|
service = SessionService()
|
|
updated = service.add_conversation_entry(
|
|
session_id="sess_123",
|
|
character_id="char_789",
|
|
character_name="Hero",
|
|
action="Talk to elder",
|
|
dm_response="The elder offers you a quest...",
|
|
quest_offered={"quest_id": "quest_goblin", "name": "Clear Goblin Cave"}
|
|
)
|
|
|
|
assert updated.conversation_history[0].quest_offered is not None
|
|
assert updated.conversation_history[0].quest_offered["quest_id"] == "quest_goblin"
|
|
|
|
def test_get_recent_history(self, mock_db, mock_appwrite, mock_character_service):
|
|
"""Test getting recent conversation history."""
|
|
# Create session with 5 conversation entries
|
|
entries = []
|
|
for i in range(5):
|
|
entries.append({
|
|
"turn": i + 1,
|
|
"character_id": "char_123",
|
|
"character_name": "Hero",
|
|
"action": f"Action {i+1}",
|
|
"dm_response": f"Response {i+1}",
|
|
"timestamp": "2025-11-21T10:00:00Z",
|
|
"combat_log": []
|
|
})
|
|
|
|
session_data = {
|
|
"session_id": "sess_123",
|
|
"session_type": "solo",
|
|
"user_id": "user_456",
|
|
"turn_number": 5,
|
|
"status": "active",
|
|
"game_state": {},
|
|
"conversation_history": entries,
|
|
"config": {},
|
|
"turn_order": [],
|
|
"current_turn": 0,
|
|
"party_member_ids": []
|
|
}
|
|
|
|
mock_document = Mock()
|
|
mock_document.data = {
|
|
'userId': 'user_456',
|
|
'sessionData': __import__('json').dumps(session_data)
|
|
}
|
|
mock_db.get_document.return_value = mock_document
|
|
|
|
service = SessionService()
|
|
recent = service.get_recent_history("sess_123", num_turns=3)
|
|
|
|
assert len(recent) == 3
|
|
assert recent[0].turn == 3 # Last 3 entries
|
|
assert recent[2].turn == 5
|
|
|
|
|
|
class TestGameStateTracking:
|
|
"""Tests for game state tracking methods."""
|
|
|
|
def test_update_location(self, mock_db, mock_appwrite, mock_character_service):
|
|
"""Test updating session location."""
|
|
session_data = {
|
|
"session_id": "sess_123",
|
|
"session_type": "solo",
|
|
"user_id": "user_456",
|
|
"turn_number": 0,
|
|
"status": "active",
|
|
"game_state": {
|
|
"current_location": "Town",
|
|
"location_type": "town",
|
|
"discovered_locations": ["Town"]
|
|
},
|
|
"conversation_history": [],
|
|
"config": {},
|
|
"turn_order": [],
|
|
"current_turn": 0,
|
|
"party_member_ids": []
|
|
}
|
|
|
|
mock_document = Mock()
|
|
mock_document.data = {
|
|
'userId': 'user_456',
|
|
'sessionData': __import__('json').dumps(session_data)
|
|
}
|
|
mock_db.get_document.return_value = mock_document
|
|
|
|
service = SessionService()
|
|
updated = service.update_location(
|
|
session_id="sess_123",
|
|
new_location="Dark Forest",
|
|
location_type=LocationType.WILDERNESS
|
|
)
|
|
|
|
assert updated.game_state.current_location == "Dark Forest"
|
|
assert updated.game_state.location_type == LocationType.WILDERNESS
|
|
assert "Dark Forest" in updated.game_state.discovered_locations
|
|
|
|
def test_add_active_quest(self, mock_db, mock_appwrite, mock_character_service):
|
|
"""Test adding an active quest."""
|
|
session_data = {
|
|
"session_id": "sess_123",
|
|
"session_type": "solo",
|
|
"user_id": "user_456",
|
|
"turn_number": 0,
|
|
"status": "active",
|
|
"game_state": {
|
|
"active_quests": [],
|
|
"current_location": "Town",
|
|
"location_type": "town"
|
|
},
|
|
"conversation_history": [],
|
|
"config": {},
|
|
"turn_order": [],
|
|
"current_turn": 0,
|
|
"party_member_ids": []
|
|
}
|
|
|
|
mock_document = Mock()
|
|
mock_document.data = {
|
|
'userId': 'user_456',
|
|
'sessionData': __import__('json').dumps(session_data)
|
|
}
|
|
mock_db.get_document.return_value = mock_document
|
|
|
|
service = SessionService()
|
|
updated = service.add_active_quest("sess_123", "quest_goblin")
|
|
|
|
assert "quest_goblin" in updated.game_state.active_quests
|
|
|
|
def test_add_active_quest_limit(self, mock_db, mock_appwrite, mock_character_service):
|
|
"""Test adding quest fails when max reached."""
|
|
session_data = {
|
|
"session_id": "sess_123",
|
|
"session_type": "solo",
|
|
"user_id": "user_456",
|
|
"turn_number": 0,
|
|
"status": "active",
|
|
"game_state": {
|
|
"active_quests": ["quest_1", "quest_2"], # Already at max
|
|
"current_location": "Town",
|
|
"location_type": "town"
|
|
},
|
|
"conversation_history": [],
|
|
"config": {},
|
|
"turn_order": [],
|
|
"current_turn": 0,
|
|
"party_member_ids": []
|
|
}
|
|
|
|
mock_document = Mock()
|
|
mock_document.data = {
|
|
'userId': 'user_456',
|
|
'sessionData': __import__('json').dumps(session_data)
|
|
}
|
|
mock_db.get_document.return_value = mock_document
|
|
|
|
service = SessionService()
|
|
with pytest.raises(SessionValidationError):
|
|
service.add_active_quest("sess_123", "quest_3")
|
|
|
|
def test_remove_active_quest(self, mock_db, mock_appwrite, mock_character_service):
|
|
"""Test removing an active quest."""
|
|
session_data = {
|
|
"session_id": "sess_123",
|
|
"session_type": "solo",
|
|
"user_id": "user_456",
|
|
"turn_number": 0,
|
|
"status": "active",
|
|
"game_state": {
|
|
"active_quests": ["quest_1", "quest_2"],
|
|
"current_location": "Town",
|
|
"location_type": "town"
|
|
},
|
|
"conversation_history": [],
|
|
"config": {},
|
|
"turn_order": [],
|
|
"current_turn": 0,
|
|
"party_member_ids": []
|
|
}
|
|
|
|
mock_document = Mock()
|
|
mock_document.data = {
|
|
'userId': 'user_456',
|
|
'sessionData': __import__('json').dumps(session_data)
|
|
}
|
|
mock_db.get_document.return_value = mock_document
|
|
|
|
service = SessionService()
|
|
updated = service.remove_active_quest("sess_123", "quest_1")
|
|
|
|
assert "quest_1" not in updated.game_state.active_quests
|
|
assert "quest_2" in updated.game_state.active_quests
|
|
|
|
def test_add_world_event(self, mock_db, mock_appwrite, mock_character_service):
|
|
"""Test adding a world event."""
|
|
session_data = {
|
|
"session_id": "sess_123",
|
|
"session_type": "solo",
|
|
"user_id": "user_456",
|
|
"turn_number": 0,
|
|
"status": "active",
|
|
"game_state": {
|
|
"world_events": [],
|
|
"current_location": "Town",
|
|
"location_type": "town"
|
|
},
|
|
"conversation_history": [],
|
|
"config": {},
|
|
"turn_order": [],
|
|
"current_turn": 0,
|
|
"party_member_ids": []
|
|
}
|
|
|
|
mock_document = Mock()
|
|
mock_document.data = {
|
|
'userId': 'user_456',
|
|
'sessionData': __import__('json').dumps(session_data)
|
|
}
|
|
mock_db.get_document.return_value = mock_document
|
|
|
|
service = SessionService()
|
|
updated = service.add_world_event("sess_123", {"type": "festival", "description": "A festival begins"})
|
|
|
|
assert len(updated.game_state.world_events) == 1
|
|
assert updated.game_state.world_events[0]["type"] == "festival"
|
|
assert "timestamp" in updated.game_state.world_events[0]
|
|
|
|
|
|
class TestSessionLifecycle:
|
|
"""Tests for session lifecycle management."""
|
|
|
|
def test_end_session(self, mock_db, mock_appwrite, mock_character_service):
|
|
"""Test ending a session."""
|
|
session_data = {
|
|
"session_id": "sess_123",
|
|
"session_type": "solo",
|
|
"user_id": "user_456",
|
|
"turn_number": 10,
|
|
"status": "active",
|
|
"game_state": {},
|
|
"conversation_history": [],
|
|
"config": {},
|
|
"turn_order": [],
|
|
"current_turn": 0,
|
|
"party_member_ids": []
|
|
}
|
|
|
|
mock_document = Mock()
|
|
mock_document.data = {
|
|
'userId': 'user_456',
|
|
'sessionData': __import__('json').dumps(session_data)
|
|
}
|
|
mock_db.get_document.return_value = mock_document
|
|
|
|
service = SessionService()
|
|
updated = service.end_session("sess_123", "user_456")
|
|
|
|
assert updated.status == SessionStatus.COMPLETED
|
|
mock_db.update_document.assert_called_once()
|
|
|
|
def test_count_user_sessions(self, mock_db, mock_appwrite, mock_character_service):
|
|
"""Test counting user sessions."""
|
|
mock_db.count_documents.return_value = 3
|
|
|
|
service = SessionService()
|
|
count = service.count_user_sessions("user_123", active_only=True)
|
|
|
|
assert count == 3
|
|
mock_db.count_documents.assert_called_once()
|