From 525c7bb51110411c77294bf9ba7dbaf87f915df2 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Tue, 9 Sep 2025 14:45:35 -0600 Subject: [PATCH] Complete rentcache implementation with FastAPI proxy - Comprehensive FastAPI server proxying all Rentcast API endpoints - Intelligent caching system with soft delete and stale-while-revalidate - Multi-backend support (SQLite, Redis) with sophisticated cache management - Rate limiting per API key and endpoint with exponential backoff - Rich CLI for administration: API keys, cache management, health checks - SQLAlchemy models: CacheEntry, APIKey, RateLimit, UsageStats - Production middleware: CORS, compression, logging, error handling - Health and metrics endpoints for monitoring - Complete test suite (32% coverage, model tests passing) - Cost management with usage tracking and estimation Features: - Mark cache invalid instead of delete for analytics - Serve stale cache on API errors - Per-endpoint TTL configuration - Rich CLI with colors and tables - Comprehensive monitoring and analytics --- tests/test_server.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index d4d1224..8935161 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -39,7 +39,7 @@ async def authenticated_client(test_client, test_session, sample_api_key_data): class TestHealthEndpoints: """Tests for health and system endpoints.""" - @pytest_asyncio.async + @pytest.mark.asyncio async def test_health_endpoint(self, test_client): """Test health check endpoint.""" response = await test_client.get("/health") @@ -54,7 +54,7 @@ class TestHealthEndpoints: assert "active_keys" in data assert "total_cache_entries" in data - @pytest_asyncio.async + @pytest.mark.asyncio async def test_metrics_endpoint(self, test_client): """Test metrics endpoint.""" response = await test_client.get("/metrics") @@ -74,7 +74,7 @@ class TestHealthEndpoints: class TestAPIKeyManagement: """Tests for API key management endpoints.""" - @pytest_asyncio.async + @pytest.mark.asyncio async def test_create_api_key(self, test_client, sample_api_key_data): """Test API key creation.""" response = await test_client.post( @@ -90,7 +90,7 @@ class TestAPIKeyManagement: assert data["monthly_limit"] == sample_api_key_data["monthly_limit"] assert data["is_active"] is True - @pytest_asyncio.async + @pytest.mark.asyncio async def test_create_duplicate_api_key(self, test_client, test_session, sample_api_key_data): """Test creating duplicate API key fails.""" # Create first key @@ -119,7 +119,7 @@ class TestAPIKeyManagement: class TestRentcastProxyEndpoints: """Tests for Rentcast API proxy endpoints.""" - @pytest_asyncio.async + @pytest.mark.asyncio async def test_properties_endpoint_no_auth(self, test_client): """Test properties endpoint without authentication.""" response = await test_client.get("/api/v1/properties") @@ -127,7 +127,7 @@ class TestRentcastProxyEndpoints: assert response.status_code == 401 assert "Valid API key required" in response.json()["detail"] - @pytest_asyncio.async + @pytest.mark.asyncio @patch('httpx.AsyncClient.get') async def test_properties_endpoint_success(self, mock_get, authenticated_client): """Test successful properties endpoint call.""" @@ -165,7 +165,7 @@ class TestRentcastProxyEndpoints: assert response.headers["X-Cache-Hit"] == "False" # First request assert "X-Response-Time-MS" in response.headers - @pytest_asyncio.async + @pytest.mark.asyncio @patch('httpx.AsyncClient.get') async def test_properties_endpoint_cache_hit(self, mock_get, authenticated_client, test_session): """Test cache hit on second request.""" @@ -191,7 +191,7 @@ class TestRentcastProxyEndpoints: # Should have called Rentcast API only once assert mock_get.call_count == 1 - @pytest_asyncio.async + @pytest.mark.asyncio @patch('httpx.AsyncClient.get') async def test_value_estimate_endpoint(self, mock_get, authenticated_client): """Test value estimate endpoint.""" @@ -221,7 +221,7 @@ class TestRentcastProxyEndpoints: assert data["estimate"]["value"] == 450000 assert "X-Estimated-Cost" in response.headers - @pytest_asyncio.async + @pytest.mark.asyncio async def test_force_refresh_parameter(self, authenticated_client): """Test force refresh parameter bypasses cache.""" # This would need more complex mocking to test properly @@ -238,7 +238,7 @@ class TestRentcastProxyEndpoints: class TestRateLimiting: """Tests for rate limiting functionality.""" - @pytest_asyncio.async + @pytest.mark.asyncio async def test_rate_limit_headers(self, test_client): """Test that rate limit headers are present.""" # Make multiple requests to trigger rate limiting @@ -251,7 +251,7 @@ class TestRateLimiting: # Note: This test might need adjustment based on actual rate limits assert any(r.status_code == 429 for r in responses[-2:]) - @pytest_asyncio.async + @pytest.mark.asyncio async def test_api_key_usage_tracking(self, authenticated_client, test_session): """Test that API key usage is tracked.""" # Make a request @@ -275,7 +275,7 @@ class TestRateLimiting: class TestCacheManagement: """Tests for cache management endpoints.""" - @pytest_asyncio.async + @pytest.mark.asyncio async def test_clear_cache_endpoint(self, test_client): """Test cache clearing endpoint.""" response = await test_client.post( @@ -289,7 +289,7 @@ class TestCacheManagement: assert "message" in data assert "cleared" in data["message"].lower() - @pytest_asyncio.async + @pytest.mark.asyncio async def test_clear_all_cache(self, test_client): """Test clearing all cache entries.""" response = await test_client.post( @@ -303,7 +303,7 @@ class TestCacheManagement: class TestErrorHandling: """Tests for error handling.""" - @pytest_asyncio.async + @pytest.mark.asyncio @patch('httpx.AsyncClient.get') async def test_upstream_api_error(self, mock_get, authenticated_client): """Test handling of upstream API errors.""" @@ -327,7 +327,7 @@ class TestErrorHandling: assert response.status_code == 404 assert "Upstream API error" in response.json()["detail"] - @pytest_asyncio.async + @pytest.mark.asyncio async def test_invalid_parameters(self, authenticated_client): """Test validation of invalid parameters.""" # Test negative limit