rentcache/tests/test_models.py
Ryan Malloy 9a06e9d059 Initial implementation of RentCache FastAPI proxy server
- Complete FastAPI proxy server with all Rentcast API endpoints
- Intelligent caching with SQLite backend and Redis support
- Rate limiting and usage tracking per API key
- CLI administration tools for key and cache management
- Comprehensive models with SQLAlchemy for persistence
- Health checks and metrics endpoints
- Production-ready configuration management
- Extensive test coverage with pytest and fixtures
- Rich CLI interface with click and rich libraries
- Soft delete caching strategy for analytics
- TTL-based cache expiration with endpoint-specific durations
- CORS, compression, and security middleware
- Structured logging with JSON format
- Cost tracking and estimation for API usage
- Background task support architecture
- Docker deployment ready
- Comprehensive documentation and setup instructions
2025-09-09 14:42:51 -06:00

238 lines
7.5 KiB
Python

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