Lemontropia-Suite/tests/test_nexus_api.py

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