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