Files
Code_of_Conquest/api/tests/test_session_service.py
Phillip Tarrant 03ab783eeb Combat Backend & Data Models
- Implement Combat Service
- Implement Damage Calculator
- Implement Effect Processor
- Implement Combat Actions
- Created Combat API Endpoints
2025-11-26 15:43:20 -06:00

569 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,
)
# 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()