Combat Backend & Data Models
- Implement Combat Service - Implement Damage Calculator - Implement Effect Processor - Implement Combat Actions - Created Combat API Endpoints
This commit is contained in:
376
api/tests/test_combat_api.py
Normal file
376
api/tests/test_combat_api.py
Normal file
@@ -0,0 +1,376 @@
|
||||
"""
|
||||
Integration tests for Combat API endpoints.
|
||||
|
||||
Tests the REST API endpoints for combat functionality.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from flask import Flask
|
||||
import json
|
||||
|
||||
from app import create_app
|
||||
from app.api.combat import combat_bp
|
||||
from app.models.combat import CombatEncounter, Combatant, CombatStatus
|
||||
from app.models.stats import Stats
|
||||
from app.models.enemy import EnemyTemplate, EnemyDifficulty
|
||||
from app.services.combat_service import CombatService, ActionResult, CombatRewards
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test Fixtures
|
||||
# =============================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
"""Create test Flask application."""
|
||||
app = create_app('development')
|
||||
app.config['TESTING'] = True
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(app):
|
||||
"""Create test client."""
|
||||
return app.test_client()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_stats():
|
||||
"""Sample stats for testing."""
|
||||
return Stats(
|
||||
strength=12,
|
||||
dexterity=14,
|
||||
constitution=10,
|
||||
intelligence=10,
|
||||
wisdom=10,
|
||||
charisma=10,
|
||||
luck=10
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_combatant(sample_stats):
|
||||
"""Sample player combatant."""
|
||||
return Combatant(
|
||||
combatant_id="test_char_001",
|
||||
name="Test Hero",
|
||||
is_player=True,
|
||||
current_hp=50,
|
||||
max_hp=50,
|
||||
current_mp=30,
|
||||
max_mp=30,
|
||||
stats=sample_stats,
|
||||
abilities=["basic_attack", "power_strike"],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_enemy_combatant(sample_stats):
|
||||
"""Sample enemy combatant."""
|
||||
return Combatant(
|
||||
combatant_id="test_goblin_0",
|
||||
name="Test Goblin",
|
||||
is_player=False,
|
||||
current_hp=25,
|
||||
max_hp=25,
|
||||
current_mp=10,
|
||||
max_mp=10,
|
||||
stats=sample_stats,
|
||||
abilities=["basic_attack"],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_encounter(sample_combatant, sample_enemy_combatant):
|
||||
"""Sample combat encounter."""
|
||||
encounter = CombatEncounter(
|
||||
encounter_id="test_encounter_001",
|
||||
combatants=[sample_combatant, sample_enemy_combatant],
|
||||
turn_order=[sample_combatant.combatant_id, sample_enemy_combatant.combatant_id],
|
||||
round_number=1,
|
||||
current_turn_index=0,
|
||||
status=CombatStatus.ACTIVE,
|
||||
)
|
||||
return encounter
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# List Enemies Endpoint Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestListEnemiesEndpoint:
|
||||
"""Tests for GET /api/v1/combat/enemies endpoint."""
|
||||
|
||||
def test_list_enemies_success(self, client):
|
||||
"""Test listing all enemy templates."""
|
||||
response = client.get('/api/v1/combat/enemies')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
|
||||
assert data['status'] == 200
|
||||
assert 'result' in data
|
||||
assert 'enemies' in data['result']
|
||||
|
||||
enemies = data['result']['enemies']
|
||||
assert isinstance(enemies, list)
|
||||
assert len(enemies) >= 6 # We have 6 sample enemies
|
||||
|
||||
# Verify enemy structure
|
||||
enemy_ids = [e['enemy_id'] for e in enemies]
|
||||
assert 'goblin' in enemy_ids
|
||||
|
||||
def test_list_enemies_filter_by_difficulty(self, client):
|
||||
"""Test filtering enemies by difficulty."""
|
||||
response = client.get('/api/v1/combat/enemies?difficulty=easy')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
|
||||
enemies = data['result']['enemies']
|
||||
for enemy in enemies:
|
||||
assert enemy['difficulty'] == 'easy'
|
||||
|
||||
def test_list_enemies_filter_by_tag(self, client):
|
||||
"""Test filtering enemies by tag."""
|
||||
response = client.get('/api/v1/combat/enemies?tag=humanoid')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
|
||||
enemies = data['result']['enemies']
|
||||
for enemy in enemies:
|
||||
assert 'humanoid' in [t.lower() for t in enemy['tags']]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Get Enemy Details Endpoint Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestGetEnemyEndpoint:
|
||||
"""Tests for GET /api/v1/combat/enemies/<enemy_id> endpoint."""
|
||||
|
||||
def test_get_enemy_success(self, client):
|
||||
"""Test getting enemy details."""
|
||||
response = client.get('/api/v1/combat/enemies/goblin')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
|
||||
assert data['status'] == 200
|
||||
# Enemy data is returned directly in result (not nested under 'enemy' key)
|
||||
assert data['result']['enemy_id'] == 'goblin'
|
||||
assert 'base_stats' in data['result']
|
||||
assert 'loot_table' in data['result']
|
||||
|
||||
def test_get_enemy_not_found(self, client):
|
||||
"""Test getting non-existent enemy."""
|
||||
response = client.get('/api/v1/combat/enemies/nonexistent_12345')
|
||||
|
||||
assert response.status_code == 404
|
||||
data = response.get_json()
|
||||
assert data['status'] == 404
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Start Combat Endpoint Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestStartCombatEndpoint:
|
||||
"""Tests for POST /api/v1/combat/start endpoint."""
|
||||
|
||||
def test_start_combat_requires_auth(self, client):
|
||||
"""Test that start combat endpoint requires authentication."""
|
||||
response = client.post(
|
||||
'/api/v1/combat/start',
|
||||
json={
|
||||
'session_id': 'test_session_001',
|
||||
'enemy_ids': ['goblin', 'goblin']
|
||||
}
|
||||
)
|
||||
|
||||
# Should return 401 Unauthorized without valid session
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_start_combat_missing_session_id(self, client):
|
||||
"""Test starting combat without session_id."""
|
||||
response = client.post(
|
||||
'/api/v1/combat/start',
|
||||
json={'enemy_ids': ['goblin']},
|
||||
)
|
||||
|
||||
assert response.status_code in [400, 401]
|
||||
|
||||
def test_start_combat_missing_enemies(self, client):
|
||||
"""Test starting combat without enemies."""
|
||||
response = client.post(
|
||||
'/api/v1/combat/start',
|
||||
json={'session_id': 'test_session'},
|
||||
)
|
||||
|
||||
assert response.status_code in [400, 401]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Execute Action Endpoint Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestExecuteActionEndpoint:
|
||||
"""Tests for POST /api/v1/combat/<session_id>/action endpoint."""
|
||||
|
||||
def test_action_requires_auth(self, client):
|
||||
"""Test that action endpoint requires authentication."""
|
||||
response = client.post(
|
||||
'/api/v1/combat/test_session/action',
|
||||
json={
|
||||
'action_type': 'attack',
|
||||
'target_ids': ['enemy_001']
|
||||
}
|
||||
)
|
||||
|
||||
# Should return 401 Unauthorized without valid session
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_action_missing_type(self, client):
|
||||
"""Test action with missing action_type still requires auth."""
|
||||
# Without auth, returns 401 regardless of payload issues
|
||||
response = client.post(
|
||||
'/api/v1/combat/test_session/action',
|
||||
json={'target_ids': ['enemy_001']}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Enemy Turn Endpoint Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestEnemyTurnEndpoint:
|
||||
"""Tests for POST /api/v1/combat/<session_id>/enemy-turn endpoint."""
|
||||
|
||||
def test_enemy_turn_requires_auth(self, client):
|
||||
"""Test that enemy turn endpoint requires authentication."""
|
||||
response = client.post('/api/v1/combat/test_session/enemy-turn')
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Flee Endpoint Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestFleeEndpoint:
|
||||
"""Tests for POST /api/v1/combat/<session_id>/flee endpoint."""
|
||||
|
||||
def test_flee_requires_auth(self, client):
|
||||
"""Test that flee endpoint requires authentication."""
|
||||
response = client.post('/api/v1/combat/test_session/flee')
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Get Combat State Endpoint Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestGetCombatStateEndpoint:
|
||||
"""Tests for GET /api/v1/combat/<session_id>/state endpoint."""
|
||||
|
||||
def test_state_requires_auth(self, client):
|
||||
"""Test that state endpoint requires authentication."""
|
||||
response = client.get('/api/v1/combat/test_session/state')
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# End Combat Endpoint Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestEndCombatEndpoint:
|
||||
"""Tests for POST /api/v1/combat/<session_id>/end endpoint."""
|
||||
|
||||
def test_end_requires_auth(self, client):
|
||||
"""Test that end combat endpoint requires authentication."""
|
||||
response = client.post('/api/v1/combat/test_session/end')
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Response Format Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestCombatAPIResponseFormat:
|
||||
"""Tests for API response format consistency."""
|
||||
|
||||
def test_enemies_response_format(self, client):
|
||||
"""Test that enemies list has standard response format."""
|
||||
response = client.get('/api/v1/combat/enemies')
|
||||
data = response.get_json()
|
||||
|
||||
# Standard response fields
|
||||
assert 'app' in data
|
||||
assert 'version' in data
|
||||
assert 'status' in data
|
||||
assert 'timestamp' in data
|
||||
assert 'result' in data
|
||||
|
||||
# Should not have error for successful request
|
||||
assert data['error'] is None or 'error' not in data or data['error'] == {}
|
||||
|
||||
def test_enemy_details_response_format(self, client):
|
||||
"""Test that enemy details has standard response format."""
|
||||
response = client.get('/api/v1/combat/enemies/goblin')
|
||||
data = response.get_json()
|
||||
|
||||
assert data['status'] == 200
|
||||
assert 'result' in data
|
||||
|
||||
# Enemy data is returned directly in result
|
||||
enemy = data['result']
|
||||
# Required enemy fields
|
||||
assert 'enemy_id' in enemy
|
||||
assert 'name' in enemy
|
||||
assert 'description' in enemy
|
||||
assert 'base_stats' in enemy
|
||||
assert 'difficulty' in enemy
|
||||
|
||||
def test_not_found_response_format(self, client):
|
||||
"""Test 404 response format."""
|
||||
response = client.get('/api/v1/combat/enemies/nonexistent_enemy_xyz')
|
||||
data = response.get_json()
|
||||
|
||||
assert data['status'] == 404
|
||||
assert 'error' in data
|
||||
assert data['error'] is not None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Content Type Tests
|
||||
# =============================================================================
|
||||
|
||||
class TestCombatAPIContentType:
|
||||
"""Tests for content type handling."""
|
||||
|
||||
def test_json_content_type_response(self, client):
|
||||
"""Test that API returns JSON content type."""
|
||||
response = client.get('/api/v1/combat/enemies')
|
||||
|
||||
assert response.content_type == 'application/json'
|
||||
|
||||
def test_accepts_json_payload(self, client):
|
||||
"""Test that API accepts JSON payloads."""
|
||||
response = client.post(
|
||||
'/api/v1/combat/start',
|
||||
data=json.dumps({
|
||||
'session_id': 'test',
|
||||
'enemy_ids': ['goblin']
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
# Should process JSON (even if auth fails)
|
||||
assert response.status_code in [200, 400, 401]
|
||||
Reference in New Issue
Block a user