""" 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, ) # Session limits are now tier-based, using a test default MAX_ACTIVE_SESSIONS_TEST = 3 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_TEST 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()