""" Tests for RentCache models. """ import pytest import pytest_asyncio from datetime import datetime, timezone, timedelta import hashlib import json from rentcache.models import ( CacheEntry, APIKey, RateLimit, UsageStats, CreateAPIKeyRequest, UpdateAPIKeyRequest ) @pytest_asyncio.fixture async def api_key(test_session): """Create a test API key.""" key_hash = hashlib.sha256("test_key_123".encode()).hexdigest() api_key = APIKey( key_name="test_key", key_hash=key_hash, daily_limit=1000, monthly_limit=10000 ) test_session.add(api_key) await test_session.commit() await test_session.refresh(api_key) return api_key @pytest_asyncio.fixture async def cache_entry(test_session): """Create a test cache entry.""" cache_entry = CacheEntry( cache_key="test_cache_key", endpoint="properties", method="GET", params_hash="abc123", params_json='{"test": "params"}', response_data='{"test": "response"}', status_code=200, headers_json='{"Content-Type": "application/json"}', expires_at=datetime.now(timezone.utc) + timedelta(hours=1), ttl_seconds=3600 ) test_session.add(cache_entry) await test_session.commit() await test_session.refresh(cache_entry) return cache_entry class TestCacheEntry: """Tests for CacheEntry model.""" def test_cache_key_generation(self): """Test cache key generation.""" endpoint = "properties" method = "GET" params = {"address": "123 Main St", "city": "Austin"} key1 = CacheEntry.generate_cache_key(endpoint, method, params) key2 = CacheEntry.generate_cache_key(endpoint, method, params) # Should be deterministic assert key1 == key2 # Should be different with different params different_params = {"address": "456 Oak Ave", "city": "Austin"} key3 = CacheEntry.generate_cache_key(endpoint, method, different_params) assert key1 != key3 @pytest.mark.asyncio async def test_cache_entry_expiration(self, cache_entry): """Test cache entry expiration logic.""" # Should not be expired initially assert not cache_entry.is_expired() # Make it expired cache_entry.expires_at = datetime.now(timezone.utc) - timedelta(minutes=1) assert cache_entry.is_expired() @pytest.mark.asyncio async def test_cache_entry_hit_increment(self, cache_entry): """Test hit counter increment.""" initial_hits = cache_entry.hit_count initial_accessed = cache_entry.last_accessed cache_entry.increment_hit() assert cache_entry.hit_count == initial_hits + 1 assert cache_entry.last_accessed != initial_accessed @pytest.mark.asyncio async def test_cache_entry_data_parsing(self, cache_entry): """Test response data parsing.""" response_data = cache_entry.get_response_data() assert response_data == {"test": "response"} params = cache_entry.get_params() assert params == {"test": "params"} class TestAPIKey: """Tests for APIKey model.""" @pytest.mark.asyncio async def test_api_key_usage_limits(self, api_key): """Test API key usage limit checking.""" # Should be able to make requests initially assert api_key.can_make_request() # Exceed daily limit api_key.daily_usage = api_key.daily_limit assert not api_key.can_make_request() # Reset daily usage, exceed monthly limit api_key.daily_usage = 0 api_key.monthly_usage = api_key.monthly_limit assert not api_key.can_make_request() @pytest.mark.asyncio async def test_api_key_expiration(self, api_key): """Test API key expiration.""" # Set expiration in the past api_key.expires_at = datetime.now(timezone.utc) - timedelta(days=1) assert not api_key.can_make_request() # Set expiration in the future api_key.expires_at = datetime.now(timezone.utc) + timedelta(days=1) assert api_key.can_make_request() @pytest.mark.asyncio async def test_api_key_inactive(self, api_key): """Test inactive API key.""" api_key.is_active = False assert not api_key.can_make_request() @pytest.mark.asyncio async def test_api_key_usage_increment(self, api_key): """Test usage increment.""" initial_daily = api_key.daily_usage initial_monthly = api_key.monthly_usage api_key.increment_usage() assert api_key.daily_usage == initial_daily + 1 assert api_key.monthly_usage == initial_monthly + 1 class TestRateLimit: """Tests for RateLimit model.""" @pytest.mark.asyncio async def test_rate_limit_checking(self, test_session, api_key): """Test rate limit checking.""" rate_limit = RateLimit( api_key_id=api_key.id, endpoint="properties", requests_per_minute=60, requests_per_hour=3600 ) test_session.add(rate_limit) await test_session.commit() # Should allow requests initially assert rate_limit.can_make_request() # Exceed minute limit rate_limit.minute_requests = rate_limit.requests_per_minute assert not rate_limit.can_make_request() # Reset minute, exceed hour limit rate_limit.minute_requests = 0 rate_limit.hour_requests = rate_limit.requests_per_hour assert not rate_limit.can_make_request() @pytest.mark.asyncio async def test_rate_limit_backoff(self, test_session, api_key): """Test exponential backoff.""" rate_limit = RateLimit( api_key_id=api_key.id, endpoint="properties", backoff_until=datetime.now(timezone.utc) + timedelta(minutes=5) ) test_session.add(rate_limit) await test_session.commit() # Should be blocked due to backoff assert not rate_limit.can_make_request() # Remove backoff rate_limit.backoff_until = None assert rate_limit.can_make_request() class TestPydanticModels: """Tests for Pydantic request/response models.""" def test_create_api_key_request_validation(self): """Test CreateAPIKeyRequest validation.""" # Valid request valid_data = { "key_name": "test_key", "rentcast_api_key": "valid_key_123", "daily_limit": 1000, "monthly_limit": 10000 } request = CreateAPIKeyRequest(**valid_data) assert request.key_name == "test_key" # Invalid key name with special characters with pytest.raises(ValueError): CreateAPIKeyRequest( key_name="test@key", rentcast_api_key="valid_key_123", daily_limit=1000, monthly_limit=10000 ) def test_update_api_key_request(self): """Test UpdateAPIKeyRequest validation.""" # Valid update with some fields update_data = { "daily_limit": 2000, "is_active": False } request = UpdateAPIKeyRequest(**update_data) assert request.daily_limit == 2000 assert request.is_active is False assert request.monthly_limit is None # Not provided