EU-Utility/tests/unit/test_nexus_api.py

511 lines
17 KiB
Python

"""
Unit tests for Nexus API service.
Tests cover:
- Singleton pattern
- Entity type enumeration
- Search methods (items, mobs, all, by type)
- Item details retrieval
- Market data retrieval
- Caching mechanism
- Rate limiting
- Error handling
"""
import pytest
from unittest.mock import MagicMock, patch, mock_open
from datetime import datetime
from core.nexus_api import (
NexusAPI, get_nexus_api, NexusAPIError, RateLimitError,
EntityType, SearchResult, ItemDetails, MarketData,
search_items, search_mobs, search_all, get_item_details, get_market_data
)
@pytest.mark.unit
class TestNexusAPISingleton:
"""Test NexusAPI singleton behavior."""
def test_singleton_instance(self, reset_singletons):
"""Test that NexusAPI is a proper singleton."""
api1 = get_nexus_api()
api2 = get_nexus_api()
assert api1 is api2
assert isinstance(api1, NexusAPI)
def test_singleton_reset(self, reset_singletons):
"""Test that singleton can be reset."""
api1 = get_nexus_api()
NexusAPI._instance = None
api2 = get_nexus_api()
assert api1 is not api2
@pytest.mark.unit
class TestEntityType:
"""Test EntityType enumeration."""
def test_entity_type_values(self):
"""Test entity type values."""
assert EntityType.ITEM.value == "items"
assert EntityType.MOB.value == "mobs"
assert EntityType.WEAPON.value == "weapons"
assert EntityType.ARMOR.value == "armors"
assert EntityType.BLUEPRINT.value == "blueprints"
def test_get_all_types(self):
"""Test getting all entity types."""
types = EntityType.get_all_types()
assert "items" in types
assert "mobs" in types
assert "all" not in types # ALL is excluded
def test_from_string_valid(self):
"""Test getting EntityType from valid string."""
assert EntityType.from_string("items") == EntityType.ITEM
assert EntityType.from_string("ITEMS") == EntityType.ITEM
assert EntityType.from_string("mobs") == EntityType.MOB
def test_from_string_invalid(self):
"""Test getting EntityType from invalid string."""
assert EntityType.from_string("invalid") is None
assert EntityType.from_string("") is None
@pytest.mark.unit
class TestNexusAPIConfiguration:
"""Test NexusAPI configuration constants."""
def test_base_configuration(self, reset_singletons):
"""Test base configuration values."""
api = get_nexus_api()
assert api.BASE_URL == "https://api.entropianexus.com"
assert api.API_VERSION == "v1"
assert api.MAX_REQUESTS_PER_SECOND == 5
assert api.MIN_REQUEST_INTERVAL == 0.2 # 1.0 / 5
assert api.MAX_RETRIES == 3
assert api.RETRY_DELAY == 1.0
def test_initialization(self, reset_singletons):
"""Test API initialization."""
api = get_nexus_api()
assert api._initialized is True
assert api._available is True
assert api._cache_ttl == 300 # 5 minutes
@pytest.mark.unit
class TestCaching:
"""Test caching functionality."""
def test_cache_storage(self, reset_singletons):
"""Test that responses are cached."""
api = get_nexus_api()
# Manually add to cache
api._cache["test_key"] = {"data": "test"}
api._cache_timestamps["test_key"] = datetime.now().timestamp()
assert "test_key" in api._cache
def test_cache_validity(self, reset_singletons):
"""Test cache validity checking."""
api = get_nexus_api()
# Valid cache entry
api._cache["valid"] = {"data": "test"}
api._cache_timestamps["valid"] = datetime.now().timestamp()
assert api._is_cache_valid("valid") is True
# Expired cache entry
api._cache["expired"] = {"data": "test"}
api._cache_timestamps["expired"] = datetime.now().timestamp() - 400 # > 300s TTL
assert api._is_cache_valid("expired") is False
# Non-existent key
assert api._is_cache_valid("nonexistent") is False
def test_clear_cache(self, reset_singletons):
"""Test clearing cache."""
api = get_nexus_api()
api._cache["key1"] = {"data": "test1"}
api._cache["key2"] = {"data": "test2"}
api.clear_cache()
assert len(api._cache) == 0
assert len(api._cache_timestamps) == 0
@pytest.mark.unit
class TestSearchMethods:
"""Test search methods."""
@patch('core.nexus_api.NexusAPI._make_request')
def test_search_items(self, mock_request, reset_singletons):
"""Test searching for items."""
mock_request.return_value = {
"results": [
{"id": "item1", "name": "Test Item 1", "type": "weapon"},
{"id": "item2", "name": "Test Item 2", "type": "armor"}
]
}
api = get_nexus_api()
results = api.search_items("test", limit=10)
assert len(results) == 2
assert results[0].id == "item1"
assert results[0].name == "Test Item 1"
mock_request.assert_called_once_with('search', {'q': 'test', 'limit': 10, 'type': 'item'})
@patch('core.nexus_api.NexusAPI._make_request')
def test_search_mobs(self, mock_request, reset_singletons):
"""Test searching for mobs."""
mock_request.return_value = {
"results": [
{"id": "mob1", "name": "Atrox", "type": "mob"}
]
}
api = get_nexus_api()
results = api.search_mobs("atrox", limit=5)
assert len(results) == 1
assert results[0].name == "Atrox"
mock_request.assert_called_once_with('search', {'q': 'atrox', 'limit': 5, 'type': 'mob'})
@patch('core.nexus_api.NexusAPI._make_request')
def test_search_all(self, mock_request, reset_singletons):
"""Test searching across all types."""
mock_request.return_value = {
"results": [
{"id": "item1", "name": "Test Item", "type": "item"},
{"id": "mob1", "name": "Test Mob", "type": "mob"}
]
}
api = get_nexus_api()
results = api.search_all("test", limit=20)
assert len(results) == 2
mock_request.assert_called_once_with('search', {'q': 'test', 'limit': 20})
@patch('core.nexus_api.NexusAPI._make_request')
def test_search_by_type_weapons(self, mock_request, reset_singletons):
"""Test searching by specific type (weapons)."""
mock_request.return_value = [
{"id": "wp1", "name": "ArMatrix LP-35", "type": "weapon"}
]
api = get_nexus_api()
results = api.search_by_type("armatrix", "weapons", limit=10)
assert len(results) == 1
assert results[0].name == "ArMatrix LP-35"
@patch('core.nexus_api.NexusAPI._make_request')
def test_search_by_type_mobs(self, mock_request, reset_singletons):
"""Test searching by specific type (mobs)."""
mock_request.return_value = [
{"id": "mob1", "name": "Atrox", "type": "mob"}
]
api = get_nexus_api()
results = api.search_by_type("atrox", "mobs", limit=10)
assert len(results) == 1
assert results[0].name == "Atrox"
@patch('core.nexus_api.NexusAPI._make_request')
def test_search_error_handling(self, mock_request, reset_singletons):
"""Test search error handling."""
mock_request.side_effect = Exception("Network error")
api = get_nexus_api()
results = api.search_items("test")
# Should return empty list on error, not raise
assert results == []
def test_search_limit_capping(self, reset_singletons):
"""Test that search limit is capped at 100."""
api = get_nexus_api()
with patch.object(api, '_make_request') as mock_request:
mock_request.return_value = {"results": []}
api.search_items("test", limit=200) # Try to request 200
# Should be capped at 100
call_args = mock_request.call_args
assert call_args[0][1]['limit'] == 100
@pytest.mark.unit
class TestItemDetails:
"""Test item details retrieval."""
@patch('core.nexus_api.NexusAPI._make_request')
def test_get_item_details_success(self, mock_request, reset_singletons):
"""Test successful item details retrieval."""
mock_request.return_value = {
"id": "armatrix_lp-35",
"name": "ArMatrix LP-35",
"description": "A laser pistol",
"category": "weapon",
"weight": 2.5,
"tt_value": 40.0,
"decay": 0.015,
"ammo_consumption": 131,
"damage": 35.0,
"range": 51.0,
"accuracy": 20.0,
"durability": 3600
}
api = get_nexus_api()
details = api.get_item_details("armatrix_lp-35")
assert details is not None
assert details.id == "armatrix_lp-35"
assert details.name == "ArMatrix LP-35"
assert details.tt_value == 40.0
assert details.damage == 35.0
@patch('core.nexus_api.NexusAPI._make_request')
def test_get_item_details_not_found(self, mock_request, reset_singletons):
"""Test item details when not found."""
mock_request.return_value = {"error": "Item not found"}
api = get_nexus_api()
details = api.get_item_details("nonexistent")
assert details is None
@patch('core.nexus_api.NexusAPI._make_request')
def test_get_item_details_error(self, mock_request, reset_singletons):
"""Test item details error handling."""
mock_request.side_effect = Exception("Network error")
api = get_nexus_api()
details = api.get_item_details("item1")
# Should return None on error
assert details is None
@pytest.mark.unit
class TestMarketData:
"""Test market data retrieval."""
@patch('core.nexus_api.NexusAPI._make_request')
def test_get_market_data_success(self, mock_request, reset_singletons):
"""Test successful market data retrieval."""
mock_request.return_value = {
"item_id": "armatrix_lp-35",
"item_name": "ArMatrix LP-35",
"current_markup": 110.5,
"avg_markup_7d": 109.0,
"avg_markup_30d": 108.5,
"volume_24h": 50,
"volume_7d": 350,
"buy_orders": [{"price": 44.0, "quantity": 10}],
"sell_orders": [{"price": 45.0, "quantity": 5}],
"last_updated": "2024-01-01T12:00:00Z"
}
api = get_nexus_api()
market = api.get_market_data("armatrix_lp-35")
assert market is not None
assert market.item_id == "armatrix_lp-35"
assert market.current_markup == 110.5
assert market.volume_24h == 50
assert len(market.buy_orders) == 1
assert len(market.sell_orders) == 1
@patch('core.nexus_api.NexusAPI._make_request')
def test_get_market_data_not_found(self, mock_request, reset_singletons):
"""Test market data when not found."""
mock_request.return_value = {"error": "Market data not found"}
api = get_nexus_api()
market = api.get_market_data("nonexistent")
assert market is None
@patch('core.nexus_api.NexusAPI._make_request')
def test_get_market_data_timestamp_parsing(self, mock_request, reset_singletons):
"""Test market data timestamp parsing."""
mock_request.return_value = {
"item_id": "item1",
"last_updated": "2024-01-15T10:30:00Z"
}
api = get_nexus_api()
market = api.get_market_data("item1")
assert market.last_updated is not None
assert isinstance(market.last_updated, datetime)
@pytest.mark.unit
class TestBatchMethods:
"""Test batch retrieval methods."""
@patch('core.nexus_api.NexusAPI.get_item_details')
def test_get_items_batch(self, mock_details, reset_singletons):
"""Test batch item details retrieval."""
mock_details.side_effect = [
ItemDetails(id="item1", name="Item 1"),
ItemDetails(id="item2", name="Item 2"),
None # One item not found
]
api = get_nexus_api()
results = api.get_items_batch(["item1", "item2", "item3"])
assert len(results) == 3
assert results["item1"].name == "Item 1"
assert results["item2"].name == "Item 2"
assert results["item3"] is None
@patch('core.nexus_api.NexusAPI.get_market_data')
def test_get_market_batch(self, mock_market, reset_singletons):
"""Test batch market data retrieval."""
mock_market.side_effect = [
MarketData(item_id="item1", current_markup=110.0),
MarketData(item_id="item2", current_markup=120.0)
]
api = get_nexus_api()
results = api.get_market_batch(["item1", "item2"])
assert len(results) == 2
assert results["item1"].current_markup == 110.0
@pytest.mark.unit
class TestEntityDetails:
"""Test generic entity details retrieval."""
@patch('core.nexus_api.NexusAPI._make_request')
def test_get_entity_details_mob(self, mock_request, reset_singletons):
"""Test getting mob entity details."""
mock_request.return_value = {
"id": "atrox",
"name": "Atrox",
"level": 25,
"threat": "medium"
}
api = get_nexus_api()
details = api.get_entity_details("atrox", "mobs")
assert details is not None
assert details["name"] == "Atrox"
@patch('core.nexus_api.NexusAPI._make_request')
def test_get_entity_details_location(self, mock_request, reset_singletons):
"""Test getting location entity details."""
mock_request.return_value = {
"id": "fort-izzuk",
"name": "Fort Izzuk",
"planet": "Calypso"
}
api = get_nexus_api()
details = api.get_entity_details("fort-izzuk", "locations")
assert details is not None
assert details["name"] == "Fort Izzuk"
@patch('core.nexus_api.NexusAPI._make_request')
def test_get_entity_details_not_found(self, mock_request, reset_singletons):
"""Test entity details when not found."""
mock_request.return_value = {"error": "Not found"}
api = get_nexus_api()
details = api.get_entity_details("nonexistent", "mobs")
assert details is None
@pytest.mark.unit
class TestConvenienceFunctions:
"""Test module-level convenience functions."""
@patch('core.nexus_api.NexusAPI.search_items')
def test_search_items_convenience(self, mock_search, reset_singletons):
"""Test search_items convenience function."""
mock_search.return_value = [SearchResult(id="item1", name="Test", type="item")]
results = search_items("test", limit=10)
assert len(results) == 1
mock_search.assert_called_once_with("test", 10)
@patch('core.nexus_api.NexusAPI.search_mobs')
def test_search_mobs_convenience(self, mock_search, reset_singletons):
"""Test search_mobs convenience function."""
mock_search.return_value = [SearchResult(id="mob1", name="Atrox", type="mob")]
results = search_mobs("atrox", limit=5)
assert len(results) == 1
mock_search.assert_called_once_with("atrox", 5)
@patch('core.nexus_api.NexusAPI.search_all')
def test_search_all_convenience(self, mock_search, reset_singletons):
"""Test search_all convenience function."""
mock_search.return_value = []
results = search_all("test", limit=20)
mock_search.assert_called_once_with("test", 20)
@patch('core.nexus_api.NexusAPI.get_item_details')
def test_get_item_details_convenience(self, mock_details, reset_singletons):
"""Test get_item_details convenience function."""
mock_details.return_value = ItemDetails(id="item1", name="Test")
result = get_item_details("item1")
assert result.name == "Test"
mock_details.assert_called_once_with("item1")
@patch('core.nexus_api.NexusAPI.get_market_data')
def test_get_market_data_convenience(self, mock_market, reset_singletons):
"""Test get_market_data convenience function."""
mock_market.return_value = MarketData(item_id="item1", current_markup=110.0)
result = get_market_data("item1")
assert result.current_markup == 110.0
mock_market.assert_called_once_with("item1")
@pytest.mark.unit
class TestAvailability:
"""Test API availability checking."""
def test_is_available(self, reset_singletons):
"""Test availability check."""
api = get_nexus_api()
assert api.is_available() is True
# Simulate unavailable
api._available = False
assert api.is_available() is False