Skip to main content
Glama

Vultr MCP

by rsp2k
test_cache.pyโ€ข29.4 kB
""" Comprehensive tests for the cache utilities module. This module provides thorough testing of caching functionality including cache manager, TTL behavior, cache decorators, and edge cases. """ import asyncio import hashlib import json import time from unittest.mock import AsyncMock, MagicMock, patch import pytest from cachetools import TTLCache from mcp_vultr.cache import ( CacheManager, cached_request, clear_cache, get_cache_manager, get_cache_stats, ) @pytest.mark.unit class TestCacheManager: """Test the CacheManager class.""" def test_init_default_parameters(self): """Test cache manager initialization with default parameters.""" cache_manager = CacheManager() assert cache_manager.max_size == 1000 assert cache_manager.default_ttl == 300 assert cache_manager.domain_ttl == 3600 assert cache_manager.record_ttl == 300 # Check cache instances assert isinstance(cache_manager.domain_cache, TTLCache) assert isinstance(cache_manager.record_cache, TTLCache) assert isinstance(cache_manager.general_cache, TTLCache) # Check cache sizes assert cache_manager.domain_cache.maxsize == 250 # max_size // 4 assert cache_manager.record_cache.maxsize == 500 # max_size // 2 assert cache_manager.general_cache.maxsize == 250 # max_size // 4 # Check initial stats expected_stats = {"hits": 0, "misses": 0, "evictions": 0, "sets": 0} assert cache_manager.stats == expected_stats def test_init_custom_parameters(self): """Test cache manager initialization with custom parameters.""" cache_manager = CacheManager( max_size=2000, default_ttl=600, domain_ttl=7200, record_ttl=900 ) assert cache_manager.max_size == 2000 assert cache_manager.default_ttl == 600 assert cache_manager.domain_ttl == 7200 assert cache_manager.record_ttl == 900 assert cache_manager.domain_cache.maxsize == 500 assert cache_manager.record_cache.maxsize == 1000 assert cache_manager.general_cache.maxsize == 500 def test_generate_key_basic(self): """Test cache key generation with basic parameters.""" cache_manager = CacheManager() key = cache_manager._generate_key("GET", "/api/domains") assert isinstance(key, str) assert len(key) == 32 # MD5 hash length # Same parameters should generate same key key2 = cache_manager._generate_key("GET", "/api/domains") assert key == key2 def test_generate_key_with_params(self): """Test cache key generation with parameters.""" cache_manager = CacheManager() params = {"page": 1, "limit": 10} key1 = cache_manager._generate_key("GET", "/api/domains", params) # Different params should generate different key different_params = {"page": 2, "limit": 10} key2 = cache_manager._generate_key("GET", "/api/domains", different_params) assert key1 != key2 # Same params in different order should generate same key reordered_params = {"limit": 10, "page": 1} key3 = cache_manager._generate_key("GET", "/api/domains", reordered_params) assert key1 == key3 def test_generate_key_deterministic(self): """Test that key generation is deterministic.""" cache_manager = CacheManager() key_data = { "method": "GET", "endpoint": "/api/domains", "params": {"sort": "name", "order": "asc"} } # Generate expected key manually key_string = json.dumps(key_data, sort_keys=True) expected_key = hashlib.md5(key_string.encode()).hexdigest() actual_key = cache_manager._generate_key("GET", "/api/domains", {"sort": "name", "order": "asc"}) assert actual_key == expected_key def test_get_cache_domain_endpoint(self): """Test cache selection for domain endpoints.""" cache_manager = CacheManager() # Domain endpoints should use domain cache cache = cache_manager._get_cache("/api/domains") assert cache is cache_manager.domain_cache cache = cache_manager._get_cache("/api/v2/domains") assert cache is cache_manager.domain_cache cache = cache_manager._get_cache("/domains/example.com") assert cache is cache_manager.domain_cache def test_get_cache_record_endpoint(self): """Test cache selection for record endpoints.""" cache_manager = CacheManager() # Record endpoints should use record cache cache = cache_manager._get_cache("/api/domains/example.com/records") assert cache is cache_manager.record_cache cache = cache_manager._get_cache("/records/123") assert cache is cache_manager.record_cache def test_get_cache_general_endpoint(self): """Test cache selection for general endpoints.""" cache_manager = CacheManager() # Other endpoints should use general cache cache = cache_manager._get_cache("/api/users") assert cache is cache_manager.general_cache cache = cache_manager._get_cache("/api/billing") assert cache is cache_manager.general_cache def test_get_cache_precedence(self): """Test cache selection precedence (records over domains).""" cache_manager = CacheManager() # URLs with both domains and records should use record cache cache = cache_manager._get_cache("/api/domains/example.com/records") assert cache is cache_manager.record_cache def test_get_success(self): """Test successful cache retrieval.""" cache_manager = CacheManager() # Manually add item to cache key = cache_manager._generate_key("GET", "/api/domains") cache_manager.domain_cache[key] = {"domain": "example.com"} result = cache_manager.get("GET", "/api/domains") assert result == {"domain": "example.com"} assert cache_manager.stats["hits"] == 1 assert cache_manager.stats["misses"] == 0 def test_get_miss(self): """Test cache miss.""" cache_manager = CacheManager() result = cache_manager.get("GET", "/api/domains") assert result is None assert cache_manager.stats["hits"] == 0 assert cache_manager.stats["misses"] == 1 def test_get_non_get_method(self): """Test that non-GET methods are not cached.""" cache_manager = CacheManager() # POST requests should not be cached result = cache_manager.get("POST", "/api/domains") assert result is None assert cache_manager.stats["hits"] == 0 assert cache_manager.stats["misses"] == 0 def test_get_with_exception(self): """Test cache get with exception handling.""" cache_manager = CacheManager() # Mock the cache to raise an exception with patch.object(cache_manager.domain_cache, 'get', side_effect=Exception("Cache error")): with patch('mcp_vultr.cache.logger') as mock_logger: result = cache_manager.get("GET", "/api/domains") assert result is None mock_logger.warning.assert_called_once() def test_set_success(self): """Test successful cache setting.""" cache_manager = CacheManager() test_data = {"domain": "example.com"} cache_manager.set("GET", "/api/domains", None, test_data) # Verify it was cached result = cache_manager.get("GET", "/api/domains") assert result == test_data assert cache_manager.stats["sets"] == 1 def test_set_non_get_method(self): """Test that non-GET methods are not cached.""" cache_manager = CacheManager() cache_manager.set("POST", "/api/domains", None, {"data": "test"}) # Should not be cached assert cache_manager.stats["sets"] == 0 def test_set_none_value(self): """Test that None values are not cached.""" cache_manager = CacheManager() cache_manager.set("GET", "/api/domains", None, None) # Should not be cached assert cache_manager.stats["sets"] == 0 def test_set_with_exception(self): """Test cache set with exception handling.""" cache_manager = CacheManager() # Mock the cache to raise an exception with patch.object(cache_manager.domain_cache, '__setitem__', side_effect=Exception("Cache error")): with patch('mcp_vultr.cache.logger') as mock_logger: cache_manager.set("GET", "/api/domains", None, {"data": "test"}) mock_logger.warning.assert_called_once() assert cache_manager.stats["sets"] == 0 def test_invalidate_all(self): """Test invalidating all caches.""" cache_manager = CacheManager() # Add items to all caches cache_manager.domain_cache["key1"] = "data1" cache_manager.record_cache["key2"] = "data2" cache_manager.general_cache["key3"] = "data3" cache_manager.invalidate() assert len(cache_manager.domain_cache) == 0 assert len(cache_manager.record_cache) == 0 assert len(cache_manager.general_cache) == 0 def test_invalidate_domain_pattern(self): """Test invalidating domain cache only.""" cache_manager = CacheManager() # Add items to all caches cache_manager.domain_cache["key1"] = "data1" cache_manager.record_cache["key2"] = "data2" cache_manager.general_cache["key3"] = "data3" cache_manager.invalidate("domain") assert len(cache_manager.domain_cache) == 0 assert len(cache_manager.record_cache) == 1 assert len(cache_manager.general_cache) == 1 def test_invalidate_record_pattern(self): """Test invalidating record cache only.""" cache_manager = CacheManager() # Add items to all caches cache_manager.domain_cache["key1"] = "data1" cache_manager.record_cache["key2"] = "data2" cache_manager.general_cache["key3"] = "data3" cache_manager.invalidate("record") assert len(cache_manager.domain_cache) == 1 assert len(cache_manager.record_cache) == 0 assert len(cache_manager.general_cache) == 1 def test_invalidate_other_pattern(self): """Test invalidating general cache for unrecognized patterns.""" cache_manager = CacheManager() # Add items to all caches cache_manager.domain_cache["key1"] = "data1" cache_manager.record_cache["key2"] = "data2" cache_manager.general_cache["key3"] = "data3" cache_manager.invalidate("users") assert len(cache_manager.domain_cache) == 1 assert len(cache_manager.record_cache) == 1 assert len(cache_manager.general_cache) == 0 def test_get_stats_empty(self): """Test getting stats from empty cache.""" cache_manager = CacheManager() stats = cache_manager.get_stats() expected = { "hits": 0, "misses": 0, "evictions": 0, "sets": 0, "domain_cache_size": 0, "record_cache_size": 0, "general_cache_size": 0, "hit_rate": 0.0 } assert stats == expected def test_get_stats_with_data(self): """Test getting stats with cache activity.""" cache_manager = CacheManager() # Add some activity cache_manager.set("GET", "/api/domains", None, {"domain": "example.com"}) cache_manager.get("GET", "/api/domains") # Hit cache_manager.get("GET", "/api/other") # Miss stats = cache_manager.get_stats() assert stats["hits"] == 1 assert stats["misses"] == 1 assert stats["sets"] == 1 assert stats["domain_cache_size"] == 1 assert stats["hit_rate"] == 0.5 # 1 hit / (1 hit + 1 miss) def test_hit_rate_calculation_edge_cases(self): """Test hit rate calculation edge cases.""" cache_manager = CacheManager() # No hits or misses stats = cache_manager.get_stats() assert stats["hit_rate"] == 0.0 # Only hits cache_manager.stats["hits"] = 5 cache_manager.stats["misses"] = 0 stats = cache_manager.get_stats() assert stats["hit_rate"] == 1.0 # Only misses cache_manager.stats["hits"] = 0 cache_manager.stats["misses"] = 3 stats = cache_manager.get_stats() assert stats["hit_rate"] == 0.0 @pytest.mark.unit class TestTTLBehavior: """Test TTL (Time To Live) behavior of caches.""" def test_cache_expiration(self): """Test that cached items expire after TTL.""" # Use very short TTL for testing cache_manager = CacheManager(default_ttl=1, domain_ttl=1, record_ttl=1) # Add item cache_manager.set("GET", "/api/domains", None, {"domain": "example.com"}) # Should be available immediately result = cache_manager.get("GET", "/api/domains") assert result == {"domain": "example.com"} # Wait for expiration (add buffer for timing variance) time.sleep(1.2) # Should be expired result = cache_manager.get("GET", "/api/domains") assert result is None def test_different_ttl_per_cache_type(self): """Test that different cache types have different TTLs.""" # Use different TTLs for testing cache_manager = CacheManager( default_ttl=60, # General cache: 1 minute domain_ttl=120, # Domain cache: 2 minutes record_ttl=30 # Record cache: 30 seconds ) # Check TTL configuration assert cache_manager.domain_cache.ttl == 120 assert cache_manager.record_cache.ttl == 30 assert cache_manager.general_cache.ttl == 60 def test_cache_size_limits(self): """Test that caches respect size limits.""" # Small cache for testing cache_manager = CacheManager(max_size=8) # 2, 4, 2 for domain, record, general # Fill domain cache beyond capacity for i in range(5): cache_manager.set("GET", f"/api/domains/{i}", None, f"domain{i}") # Should only have 2 items (maxsize of domain cache) assert len(cache_manager.domain_cache) == 2 @pytest.mark.unit class TestCachedRequestDecorator: """Test the cached_request decorator.""" @pytest.mark.asyncio async def test_decorator_basic_usage(self): """Test basic decorator usage.""" cache_manager = CacheManager() call_count = 0 @cached_request(cache_manager=cache_manager) async def api_call(self, method, endpoint, params=None): nonlocal call_count call_count += 1 return f"response-{call_count}" # First call should hit the function result1 = await api_call(None, "GET", "/api/domains") assert result1 == "response-1" assert call_count == 1 # Second call should use cache result2 = await api_call(None, "GET", "/api/domains") assert result2 == "response-1" # Same result from cache assert call_count == 1 # Function not called again @pytest.mark.asyncio async def test_decorator_different_endpoints(self): """Test decorator with different endpoints.""" cache_manager = CacheManager() call_count = 0 @cached_request(cache_manager=cache_manager) async def api_call(self, method, endpoint, params=None): nonlocal call_count call_count += 1 return f"response-{endpoint}-{call_count}" # Different endpoints should not use each other's cache result1 = await api_call(None, "GET", "/api/domains") result2 = await api_call(None, "GET", "/api/records") assert result1 == "response-/api/domains-1" assert result2 == "response-/api/records-2" assert call_count == 2 @pytest.mark.asyncio async def test_decorator_with_params(self): """Test decorator with parameters.""" cache_manager = CacheManager() call_count = 0 @cached_request(cache_manager=cache_manager) async def api_call(self, method, endpoint, params=None): nonlocal call_count call_count += 1 return f"response-{params}-{call_count}" # Same endpoint with different params should not share cache result1 = await api_call(None, "GET", "/api/domains", {"page": 1}) result2 = await api_call(None, "GET", "/api/domains", {"page": 2}) result3 = await api_call(None, "GET", "/api/domains", {"page": 1}) # Should use cache assert result1 == "response-{'page': 1}-1" assert result2 == "response-{'page': 2}-2" assert result3 == "response-{'page': 1}-1" # From cache assert call_count == 2 @pytest.mark.asyncio async def test_decorator_non_get_methods(self): """Test that decorator doesn't cache non-GET methods.""" cache_manager = CacheManager() call_count = 0 @cached_request(cache_manager=cache_manager) async def api_call(self, method, endpoint, params=None): nonlocal call_count call_count += 1 return f"response-{method}-{call_count}" # POST should not be cached result1 = await api_call(None, "POST", "/api/domains") result2 = await api_call(None, "POST", "/api/domains") assert result1 == "response-POST-1" assert result2 == "response-POST-2" assert call_count == 2 @pytest.mark.asyncio async def test_decorator_with_kwargs_method(self): """Test decorator when method is passed as keyword argument.""" cache_manager = CacheManager() call_count = 0 @cached_request(cache_manager=cache_manager) async def api_call(self, **kwargs): nonlocal call_count call_count += 1 method = kwargs.get("method", "GET") endpoint = kwargs.get("endpoint", "") return f"response-{method}-{endpoint}-{call_count}" # Test with kwargs result1 = await api_call(None, method="GET", endpoint="/api/domains") result2 = await api_call(None, method="GET", endpoint="/api/domains") assert result1 == "response-GET-/api/domains-1" assert result2 == "response-GET-/api/domains-1" # From cache assert call_count == 1 @pytest.mark.asyncio async def test_decorator_default_cache_manager(self): """Test decorator with default cache manager.""" call_count = 0 @cached_request() # No cache_manager specified async def api_call(self, method, endpoint, params=None): nonlocal call_count call_count += 1 return f"response-{call_count}" # Should use global cache manager result1 = await api_call(None, "GET", "/api/domains") result2 = await api_call(None, "GET", "/api/domains") assert result1 == "response-1" assert result2 == "response-1" # From cache assert call_count == 1 @pytest.mark.unit class TestGlobalCacheFunctions: """Test the global cache management functions.""" def test_get_cache_manager(self): """Test getting the global cache manager.""" manager = get_cache_manager() assert isinstance(manager, CacheManager) # Should return the same instance manager2 = get_cache_manager() assert manager is manager2 def test_clear_cache_function(self): """Test the global clear_cache function.""" # Add some data to global cache manager = get_cache_manager() manager.set("GET", "/api/test", None, {"data": "test"}) # Clear all clear_cache() # Should be cleared result = manager.get("GET", "/api/test") assert result is None def test_clear_cache_with_pattern(self): """Test clearing cache with pattern.""" manager = get_cache_manager() # Add data to different caches manager.set("GET", "/api/domains", None, {"type": "domain"}) manager.set("GET", "/api/domains/test/records", None, {"type": "record"}) # Clear only domain cache clear_cache("domain") # Domain should be cleared, record should remain assert manager.get("GET", "/api/domains") is None assert manager.get("GET", "/api/domains/test/records") == {"type": "record"} def test_get_cache_stats_function(self): """Test the global get_cache_stats function.""" manager = get_cache_manager() # Clear any existing state manager.invalidate() manager.stats = {"hits": 0, "misses": 0, "evictions": 0, "sets": 0} # Add some activity manager.set("GET", "/api/test", None, {"data": "test"}) stats = get_cache_stats() assert stats["sets"] == 1 assert isinstance(stats, dict) @pytest.mark.unit class TestCacheEvictionAndMemoryManagement: """Test cache eviction and memory management behavior.""" def test_lru_eviction(self): """Test LRU eviction behavior.""" # Small cache size for testing cache_manager = CacheManager(max_size=4) # 1, 2, 1 for domain, record, general # Fill domain cache (maxsize is 1 for domain cache) cache_manager.set("GET", "/api/domains/1", None, "domain1") assert cache_manager.get("GET", "/api/domains/1") == "domain1" # Should be there cache_manager.set("GET", "/api/domains/2", None, "domain2") # Should evict domain1 # Check eviction happened (domain cache has maxsize=1) assert cache_manager.get("GET", "/api/domains/1") is None # Evicted assert cache_manager.get("GET", "/api/domains/2") == "domain2" # Still there def test_cache_size_distribution(self): """Test cache size distribution among different cache types.""" cache_manager = CacheManager(max_size=100) assert cache_manager.domain_cache.maxsize == 25 # 100 // 4 assert cache_manager.record_cache.maxsize == 50 # 100 // 2 assert cache_manager.general_cache.maxsize == 25 # 100 // 4 total_capacity = ( cache_manager.domain_cache.maxsize + cache_manager.record_cache.maxsize + cache_manager.general_cache.maxsize ) assert total_capacity == 100 def test_concurrent_access_safety(self): """Test that cache operations are safe under concurrent access.""" cache_manager = CacheManager() # Simulate concurrent operations def concurrent_operations(): for i in range(10): cache_manager.set("GET", f"/api/test/{i}", None, f"data{i}") cache_manager.get("GET", f"/api/test/{i}") # This should not raise any exceptions concurrent_operations() # Verify some data is there assert cache_manager.get("GET", "/api/test/9") == "data9" @pytest.mark.integration class TestCacheIntegrationScenarios: """Test realistic cache integration scenarios.""" @pytest.mark.asyncio async def test_api_client_cache_integration(self): """Test cache integration with a simulated API client.""" cache_manager = CacheManager(default_ttl=1) # Short TTL for testing api_call_count = 0 @cached_request(cache_manager=cache_manager) async def simulated_api_client(self, method, endpoint, params=None): nonlocal api_call_count api_call_count += 1 # Simulate API response if "domains" in endpoint: return {"domains": [f"domain{api_call_count}.com"]} elif "records" in endpoint: return {"records": [{"id": api_call_count, "type": "A"}]} else: return {"data": f"response{api_call_count}"} client = None # Test caching behavior response1 = await simulated_api_client(client, "GET", "/api/domains") response2 = await simulated_api_client(client, "GET", "/api/domains") # From cache assert response1 == response2 assert api_call_count == 1 # Different endpoint should not use cache response3 = await simulated_api_client(client, "GET", "/api/records") assert api_call_count == 2 # Wait for cache expiration (add buffer for timing variance) time.sleep(1.2) # Should call API again after expiration response4 = await simulated_api_client(client, "GET", "/api/domains") assert api_call_count == 3 assert response4 != response1 # Different response def test_cache_performance_under_load(self): """Test cache performance under simulated load.""" cache_manager = CacheManager(max_size=1000) # Simulate high load start_time = time.time() for i in range(500): # Mix of sets and gets cache_manager.set("GET", f"/api/endpoint/{i % 100}", None, f"data{i}") cache_manager.get("GET", f"/api/endpoint/{i % 50}") elapsed = time.time() - start_time # Should complete quickly (performance regression test) assert elapsed < 1.0 # Should be much faster, but allowing buffer # Check that cache is working stats = cache_manager.get_stats() assert stats["hits"] > 0 assert stats["sets"] > 0 def test_memory_efficiency(self): """Test memory efficiency with large amounts of cached data.""" cache_manager = CacheManager(max_size=100) # Small for testing # Add more data than cache can hold for i in range(200): cache_manager.set("GET", f"/api/test/{i}", None, f"data{i}" * 100) # Larger data # Cache should not exceed size limits total_items = ( len(cache_manager.domain_cache) + len(cache_manager.record_cache) + len(cache_manager.general_cache) ) # Should be at or below the total capacity total_capacity = ( cache_manager.domain_cache.maxsize + cache_manager.record_cache.maxsize + cache_manager.general_cache.maxsize ) assert total_items <= total_capacity def test_cache_invalidation_patterns(self): """Test common cache invalidation patterns.""" cache_manager = CacheManager() # Populate caches with different types of data cache_manager.set("GET", "/api/domains", None, {"domains": ["example.com"]}) cache_manager.set("GET", "/api/domains/example.com/records", None, {"records": []}) cache_manager.set("GET", "/api/users", None, {"users": ["user1"]}) cache_manager.set("GET", "/api/billing", None, {"balance": 100}) # Verify all data is cached assert cache_manager.get("GET", "/api/domains") is not None assert cache_manager.get("GET", "/api/domains/example.com/records") is not None assert cache_manager.get("GET", "/api/users") is not None assert cache_manager.get("GET", "/api/billing") is not None # Invalidate only domain-related data cache_manager.invalidate("domain") # Domain cache should be cleared assert cache_manager.get("GET", "/api/domains") is None # But records and other data should remain # Note: "/api/domains/example.com/records" goes to record cache, not domain cache assert cache_manager.get("GET", "/api/domains/example.com/records") is not None assert cache_manager.get("GET", "/api/users") is not None assert cache_manager.get("GET", "/api/billing") is not None # Invalidate record data cache_manager.invalidate("record") # Now records should be cleared too assert cache_manager.get("GET", "/api/domains/example.com/records") is None # But other data should still remain assert cache_manager.get("GET", "/api/users") is not None assert cache_manager.get("GET", "/api/billing") is not None

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rsp2k/mcp-vultr'

If you have feedback or need assistance with the MCP directory API, please join our Discord server