511 lines
17 KiB
Python
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
|