589 lines
19 KiB
Python
589 lines
19 KiB
Python
"""
|
|
Unit tests for Entropia Nexus API client.
|
|
|
|
Run with: python -m pytest tests/test_nexus_api.py -v
|
|
"""
|
|
|
|
import asyncio
|
|
import pytest
|
|
from decimal import Decimal
|
|
|
|
# Import the module under test
|
|
try:
|
|
from core.nexus_api import (
|
|
WeaponStats,
|
|
ArmorStats,
|
|
ToolStats,
|
|
EntropiaNexusAPI,
|
|
SimpleCache,
|
|
MOCK_WEAPONS,
|
|
MOCK_ARMORS,
|
|
MOCK_TOOLS,
|
|
get_mock_weapons,
|
|
get_mock_armors,
|
|
get_mock_tools,
|
|
calculate_dpp,
|
|
)
|
|
except ImportError:
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
from core.nexus_api import (
|
|
WeaponStats,
|
|
ArmorStats,
|
|
ToolStats,
|
|
EntropiaNexusAPI,
|
|
SimpleCache,
|
|
MOCK_WEAPONS,
|
|
MOCK_ARMORS,
|
|
MOCK_TOOLS,
|
|
get_mock_weapons,
|
|
get_mock_armors,
|
|
get_mock_tools,
|
|
calculate_dpp,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Fixtures
|
|
# =============================================================================
|
|
|
|
@pytest.fixture
|
|
def mock_api():
|
|
"""Create API client in mock mode."""
|
|
return EntropiaNexusAPI(mock_mode=True)
|
|
|
|
|
|
@pytest.fixture
|
|
def api_with_fallback():
|
|
"""Create API client with mock fallback enabled."""
|
|
return EntropiaNexusAPI(use_mock_fallback=True)
|
|
|
|
|
|
@pytest.fixture
|
|
def cache():
|
|
"""Create a fresh cache instance."""
|
|
return SimpleCache(default_ttl=3600)
|
|
|
|
|
|
# =============================================================================
|
|
# WeaponStats Tests
|
|
# =============================================================================
|
|
|
|
class TestWeaponStats:
|
|
"""Test WeaponStats data class."""
|
|
|
|
def test_weapon_creation(self):
|
|
"""Test basic weapon stats creation."""
|
|
weapon = WeaponStats(
|
|
name="Test Weapon",
|
|
damage=Decimal("10.0"),
|
|
decay_pec=Decimal("0.5"),
|
|
ammo_pec=Decimal("1.5"),
|
|
dpp=Decimal("5.0"),
|
|
range=50,
|
|
attacks_per_min=60
|
|
)
|
|
|
|
assert weapon.name == "Test Weapon"
|
|
assert weapon.damage == Decimal("10.0")
|
|
assert weapon.decay_pec == Decimal("0.5")
|
|
assert weapon.ammo_pec == Decimal("1.5")
|
|
assert weapon.dpp == Decimal("5.0")
|
|
assert weapon.range == 50
|
|
assert weapon.attacks_per_min == 60
|
|
|
|
def test_total_cost_calculation(self):
|
|
"""Test total cost per shot is calculated correctly."""
|
|
weapon = WeaponStats(
|
|
name="Test",
|
|
damage=Decimal("10.0"),
|
|
decay_pec=Decimal("0.5"),
|
|
ammo_pec=Decimal("1.5"),
|
|
dpp=Decimal("5.0")
|
|
)
|
|
|
|
expected_total = Decimal("2.0")
|
|
assert weapon.total_cost_pec == expected_total
|
|
|
|
def test_cost_per_hour_calculation(self):
|
|
"""Test cost per hour calculation."""
|
|
weapon = WeaponStats(
|
|
name="Test",
|
|
damage=Decimal("10.0"),
|
|
decay_pec=Decimal("1.0"), # 1 PEC
|
|
ammo_pec=Decimal("1.0"), # 1 PEC
|
|
dpp=Decimal("5.0"),
|
|
attacks_per_min=60 # 60 shots/min = 3600/hour
|
|
)
|
|
|
|
# Total 2 PEC per shot * 3600 shots = 7200 PEC = 72 PED
|
|
expected_cost = Decimal("72.0")
|
|
assert weapon.calculate_cost_per_hour() == expected_cost
|
|
|
|
def test_cost_per_hour_zero_attacks(self):
|
|
"""Test cost per hour with zero attacks per minute."""
|
|
weapon = WeaponStats(
|
|
name="Test",
|
|
damage=Decimal("10.0"),
|
|
decay_pec=Decimal("1.0"),
|
|
ammo_pec=Decimal("1.0"),
|
|
dpp=Decimal("5.0"),
|
|
attacks_per_min=0
|
|
)
|
|
|
|
assert weapon.calculate_cost_per_hour() == Decimal("0")
|
|
|
|
def test_to_dict_and_from_dict(self):
|
|
"""Test serialization roundtrip."""
|
|
original = WeaponStats(
|
|
name="Test Weapon",
|
|
damage=Decimal("10.0"),
|
|
decay_pec=Decimal("0.5"),
|
|
ammo_pec=Decimal("1.5"),
|
|
dpp=Decimal("5.0"),
|
|
range=50,
|
|
attacks_per_min=60,
|
|
item_id="test_weapon"
|
|
)
|
|
|
|
data = original.to_dict()
|
|
restored = WeaponStats.from_dict(data)
|
|
|
|
assert restored.name == original.name
|
|
assert restored.damage == original.damage
|
|
assert restored.decay_pec == original.decay_pec
|
|
assert restored.ammo_pec == original.ammo_pec
|
|
assert restored.dpp == original.dpp
|
|
assert restored.range == original.range
|
|
assert restored.attacks_per_min == original.attacks_per_min
|
|
assert restored.item_id == original.item_id
|
|
|
|
|
|
# =============================================================================
|
|
# ArmorStats Tests
|
|
# =============================================================================
|
|
|
|
class TestArmorStats:
|
|
"""Test ArmorStats data class."""
|
|
|
|
def test_armor_creation(self):
|
|
"""Test basic armor stats creation."""
|
|
armor = ArmorStats(
|
|
name="Test Armor",
|
|
decay_pec=Decimal("0.5"),
|
|
protection={
|
|
"impact": Decimal("10.0"),
|
|
"cut": Decimal("8.0"),
|
|
"burn": Decimal("5.0")
|
|
},
|
|
slot="body",
|
|
durability=5000
|
|
)
|
|
|
|
assert armor.name == "Test Armor"
|
|
assert armor.decay_pec == Decimal("0.5")
|
|
assert armor.protection["impact"] == Decimal("10.0")
|
|
assert armor.slot == "body"
|
|
assert armor.durability == 5000
|
|
|
|
def test_get_protection(self):
|
|
"""Test getting protection for specific damage type."""
|
|
armor = ArmorStats(
|
|
name="Test",
|
|
decay_pec=Decimal("0.5"),
|
|
protection={"impact": Decimal("10.0"), "cut": Decimal("8.0")}
|
|
)
|
|
|
|
assert armor.get_protection("impact") == Decimal("10.0")
|
|
assert armor.get_protection("cut") == Decimal("8.0")
|
|
assert armor.get_protection("nonexistent") == Decimal("0")
|
|
|
|
def test_get_total_protection(self):
|
|
"""Test total protection calculation."""
|
|
armor = ArmorStats(
|
|
name="Test",
|
|
decay_pec=Decimal("0.5"),
|
|
protection={
|
|
"impact": Decimal("10.0"),
|
|
"cut": Decimal("8.0"),
|
|
"burn": Decimal("5.0")
|
|
}
|
|
)
|
|
|
|
assert armor.get_total_protection() == Decimal("23.0")
|
|
|
|
def test_to_dict_and_from_dict(self):
|
|
"""Test serialization roundtrip."""
|
|
original = ArmorStats(
|
|
name="Test Armor",
|
|
decay_pec=Decimal("0.5"),
|
|
protection={"impact": Decimal("10.0")},
|
|
slot="head",
|
|
durability=3000,
|
|
item_id="test_armor"
|
|
)
|
|
|
|
data = original.to_dict()
|
|
restored = ArmorStats.from_dict(data)
|
|
|
|
assert restored.name == original.name
|
|
assert restored.decay_pec == original.decay_pec
|
|
assert restored.protection == original.protection
|
|
assert restored.slot == original.slot
|
|
assert restored.durability == original.durability
|
|
|
|
|
|
# =============================================================================
|
|
# ToolStats Tests
|
|
# =============================================================================
|
|
|
|
class TestToolStats:
|
|
"""Test ToolStats data class."""
|
|
|
|
def test_tool_creation(self):
|
|
"""Test basic tool stats creation."""
|
|
tool = ToolStats(
|
|
name="Test Finder",
|
|
depth=Decimal("200.0"),
|
|
radius=Decimal("25.0"),
|
|
decay_pec=Decimal("0.3"),
|
|
tool_type="finder",
|
|
probe_cost=Decimal("0.5")
|
|
)
|
|
|
|
assert tool.name == "Test Finder"
|
|
assert tool.depth == Decimal("200.0")
|
|
assert tool.radius == Decimal("25.0")
|
|
assert tool.decay_pec == Decimal("0.3")
|
|
assert tool.tool_type == "finder"
|
|
assert tool.probe_cost == Decimal("0.5")
|
|
|
|
def test_cost_per_drop_calculation(self):
|
|
"""Test cost per drop calculation."""
|
|
tool = ToolStats(
|
|
name="Test",
|
|
depth=Decimal("200.0"),
|
|
radius=Decimal("25.0"),
|
|
decay_pec=Decimal("20.0"), # 20 PEC
|
|
probe_cost=Decimal("0.5") # 0.5 PED
|
|
)
|
|
|
|
# 20 PEC = 0.2 PED + 0.5 PED probe = 0.7 PED
|
|
expected_cost = Decimal("0.7")
|
|
assert tool.calculate_cost_per_drop() == expected_cost
|
|
|
|
def test_to_dict_and_from_dict(self):
|
|
"""Test serialization roundtrip."""
|
|
original = ToolStats(
|
|
name="Test Tool",
|
|
depth=Decimal("200.0"),
|
|
radius=Decimal("25.0"),
|
|
decay_pec=Decimal("0.3"),
|
|
tool_type="finder",
|
|
probe_cost=Decimal("0.5"),
|
|
item_id="test_tool"
|
|
)
|
|
|
|
data = original.to_dict()
|
|
restored = ToolStats.from_dict(data)
|
|
|
|
assert restored.name == original.name
|
|
assert restored.depth == original.depth
|
|
assert restored.radius == original.radius
|
|
assert restored.decay_pec == original.decay_pec
|
|
assert restored.tool_type == original.tool_type
|
|
assert restored.probe_cost == original.probe_cost
|
|
|
|
|
|
# =============================================================================
|
|
# SimpleCache Tests
|
|
# =============================================================================
|
|
|
|
class TestSimpleCache:
|
|
"""Test SimpleCache implementation."""
|
|
|
|
def test_cache_set_and_get(self, cache):
|
|
"""Test basic cache operations."""
|
|
cache.set("key1", "value1")
|
|
assert cache.get("key1") == "value1"
|
|
|
|
def test_cache_miss(self, cache):
|
|
"""Test cache miss returns None."""
|
|
assert cache.get("nonexistent") is None
|
|
|
|
def test_cache_delete(self, cache):
|
|
"""Test cache deletion."""
|
|
cache.set("key1", "value1")
|
|
cache.delete("key1")
|
|
assert cache.get("key1") is None
|
|
|
|
def test_cache_clear(self, cache):
|
|
"""Test cache clear."""
|
|
cache.set("key1", "value1")
|
|
cache.set("key2", "value2")
|
|
cache.clear()
|
|
assert cache.get("key1") is None
|
|
assert cache.get("key2") is None
|
|
assert cache.keys() == []
|
|
|
|
def test_cache_keys(self, cache):
|
|
"""Test getting cache keys."""
|
|
cache.set("key1", "value1")
|
|
cache.set("key2", "value2")
|
|
keys = cache.keys()
|
|
assert "key1" in keys
|
|
assert "key2" in keys
|
|
|
|
|
|
# =============================================================================
|
|
# EntropiaNexusAPI Tests - Mock Mode
|
|
# =============================================================================
|
|
|
|
class TestEntropiaNexusAPIMock:
|
|
"""Test API client in mock mode."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_items_mock(self, mock_api):
|
|
"""Test searching items in mock mode."""
|
|
results = await mock_api.search_items("opalo")
|
|
|
|
assert len(results) > 0
|
|
assert any("opalo" in r['name'].lower() for r in results)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_items_type_filter(self, mock_api):
|
|
"""Test searching with type filter."""
|
|
results = await mock_api.search_items("", item_type="weapon")
|
|
|
|
assert all(r['type'] == 'weapon' for r in results)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_weapon_stats_mock(self, mock_api):
|
|
"""Test getting weapon stats in mock mode."""
|
|
weapon = await mock_api.get_weapon_stats("sollomate_opalo")
|
|
|
|
assert weapon is not None
|
|
assert weapon.name == "Sollomate Opalo"
|
|
assert weapon.damage > 0
|
|
assert weapon.dpp > 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_weapon_stats_partial_match(self, mock_api):
|
|
"""Test getting weapon by partial name match."""
|
|
weapon = await mock_api.get_weapon_stats("opalo")
|
|
|
|
assert weapon is not None
|
|
assert "Opalo" in weapon.name
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_weapon_stats_not_found(self, mock_api):
|
|
"""Test getting non-existent weapon."""
|
|
weapon = await mock_api.get_weapon_stats("nonexistent_weapon_xyz")
|
|
|
|
assert weapon is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_armor_stats_mock(self, mock_api):
|
|
"""Test getting armor stats in mock mode."""
|
|
armor = await mock_api.get_armor_stats("pixie")
|
|
|
|
assert armor is not None
|
|
assert armor.name == "Pixie"
|
|
assert len(armor.protection) > 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_armor_stats_partial_match(self, mock_api):
|
|
"""Test getting armor by partial name match."""
|
|
armor = await mock_api.get_armor_stats("shog")
|
|
|
|
assert armor is not None
|
|
assert "Shogun" in armor.name
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_tool_stats_mock(self, mock_api):
|
|
"""Test getting tool stats in mock mode."""
|
|
tool = await mock_api.get_tool_stats("ziplex_z1")
|
|
|
|
assert tool is not None
|
|
assert "Ziplex" in tool.name
|
|
assert tool.depth > 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cache_usage(self, mock_api):
|
|
"""Test that results are cached."""
|
|
# First call
|
|
weapon1 = await mock_api.get_weapon_stats("sollomate_opalo")
|
|
|
|
# Second call should come from cache
|
|
weapon2 = await mock_api.get_weapon_stats("sollomate_opalo")
|
|
|
|
# Same object from cache
|
|
assert weapon1 is weapon2
|
|
assert "weapon:sollomate_opalo" in mock_api._cache.keys()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clear_cache(self, mock_api):
|
|
"""Test clearing cache."""
|
|
await mock_api.get_weapon_stats("sollomate_opalo")
|
|
mock_api.clear_cache()
|
|
|
|
assert mock_api._cache.keys() == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_context_manager(self):
|
|
"""Test async context manager."""
|
|
async with EntropiaNexusAPI(mock_mode=True) as api:
|
|
weapon = await api.get_weapon_stats("sollomate_opalo")
|
|
assert weapon is not None
|
|
|
|
|
|
# =============================================================================
|
|
# Mock Data Tests
|
|
# =============================================================================
|
|
|
|
class TestMockData:
|
|
"""Test mock data integrity."""
|
|
|
|
def test_mock_weapons_count(self):
|
|
"""Test we have expected number of mock weapons."""
|
|
assert len(MOCK_WEAPONS) == 6
|
|
|
|
def test_mock_armors_count(self):
|
|
"""Test we have expected number of mock armors."""
|
|
assert len(MOCK_ARMORS) == 5
|
|
|
|
def test_mock_tools_count(self):
|
|
"""Test we have expected number of mock tools."""
|
|
assert len(MOCK_TOOLS) == 4
|
|
|
|
def test_mock_weapon_dpp_calculated(self):
|
|
"""Test mock weapons have reasonable DPP values."""
|
|
for weapon_id, weapon in MOCK_WEAPONS.items():
|
|
assert weapon.dpp > 0, f"{weapon_id} has invalid DPP"
|
|
assert weapon.damage > 0, f"{weapon_id} has invalid damage"
|
|
assert weapon.decay_pec >= 0, f"{weapon_id} has invalid decay"
|
|
|
|
def test_mock_armor_protection(self):
|
|
"""Test mock armors have protection values."""
|
|
for armor_id, armor in MOCK_ARMORS.items():
|
|
assert len(armor.protection) > 0, f"{armor_id} has no protection"
|
|
assert all(v >= 0 for v in armor.protection.values()), f"{armor_id} has negative protection"
|
|
|
|
def test_mock_tool_depth(self):
|
|
"""Test mock tools have depth values."""
|
|
for tool_id, tool in MOCK_TOOLS.items():
|
|
if tool.tool_type == "finder":
|
|
assert tool.depth > 0, f"{tool_id} finder has no depth"
|
|
assert tool.radius > 0, f"{tool_id} finder has no radius"
|
|
|
|
def test_get_mock_weapons(self):
|
|
"""Test get_mock_weapons function."""
|
|
weapons = get_mock_weapons()
|
|
assert len(weapons) == 6
|
|
assert "sollomate_opalo" in weapons
|
|
|
|
def test_get_mock_armors(self):
|
|
"""Test get_mock_armors function."""
|
|
armors = get_mock_armors()
|
|
assert len(armors) == 5
|
|
assert "pixie" in armors
|
|
|
|
def test_get_mock_tools(self):
|
|
"""Test get_mock_tools function."""
|
|
tools = get_mock_tools()
|
|
assert len(tools) == 4
|
|
assert "ziplex_z1" in tools
|
|
|
|
|
|
# =============================================================================
|
|
# Utility Function Tests
|
|
# =============================================================================
|
|
|
|
class TestUtilityFunctions:
|
|
"""Test utility functions."""
|
|
|
|
def test_calculate_dpp(self):
|
|
"""Test DPP calculation."""
|
|
damage = Decimal("10.0")
|
|
decay = Decimal("1.0") # 1 PEC
|
|
ammo = Decimal("1.0") # 1 PEC
|
|
|
|
# Total cost = 2 PEC
|
|
# DPP = damage / total_pec = 10 / 2 = 5.0
|
|
dpp = calculate_dpp(damage, decay, ammo)
|
|
assert dpp == Decimal("5.0")
|
|
|
|
def test_calculate_dpp_zero_cost(self):
|
|
"""Test DPP calculation with zero cost."""
|
|
dpp = calculate_dpp(Decimal("10.0"), Decimal("0"), Decimal("0"))
|
|
assert dpp == Decimal("0")
|
|
|
|
def test_calculate_dpp_matches_mock_weapons(self):
|
|
"""Test DPP calculation matches stored DPP values."""
|
|
for weapon in MOCK_WEAPONS.values():
|
|
calculated = calculate_dpp(
|
|
weapon.damage,
|
|
weapon.decay_pec,
|
|
weapon.ammo_pec
|
|
)
|
|
# Allow small rounding differences
|
|
diff = abs(calculated - weapon.dpp)
|
|
assert diff < Decimal("0.1"), f"DPP mismatch for {weapon.name}"
|
|
|
|
|
|
# =============================================================================
|
|
# Integration Tests
|
|
# =============================================================================
|
|
|
|
class TestIntegration:
|
|
"""Integration tests for the API client."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_workflow_hunting(self):
|
|
"""Test complete hunting setup workflow."""
|
|
async with EntropiaNexusAPI(mock_mode=True) as api:
|
|
# Search for weapon
|
|
results = await api.search_items("opalo", item_type="weapon")
|
|
assert len(results) > 0
|
|
|
|
# Get weapon details
|
|
weapon_id = results[0]['id']
|
|
weapon = await api.get_weapon_stats(weapon_id)
|
|
assert weapon is not None
|
|
|
|
# Get armor
|
|
armor = await api.get_armor_stats("pixie")
|
|
assert armor is not None
|
|
|
|
# Calculate costs
|
|
weapon_cost = weapon.calculate_cost_per_hour()
|
|
assert weapon_cost > 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_workflow_mining(self):
|
|
"""Test complete mining setup workflow."""
|
|
async with EntropiaNexusAPI(mock_mode=True) as api:
|
|
# Get finder
|
|
finder = await api.get_tool_stats("ziplex_z1")
|
|
assert finder is not None
|
|
|
|
# Get extractor
|
|
extractor = await api.get_tool_stats("ore_extractor_md1")
|
|
assert extractor is not None
|
|
|
|
# Calculate costs
|
|
finder_cost = finder.calculate_cost_per_drop()
|
|
extractor_cost = extractor.calculate_cost_per_drop()
|
|
|
|
assert finder_cost > 0
|
|
assert extractor_cost > 0
|
|
|
|
|
|
# =============================================================================
|
|
# Run Tests
|
|
# =============================================================================
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|