Files
Code_of_Conquest/api/tests/test_combat_api.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

377 lines
12 KiB
Python

"""
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]