Files
enclava/backend/tests/e2e/test_nginx_routing.py
2025-08-25 17:13:15 +02:00

241 lines
11 KiB
Python

"""
Nginx reverse proxy routing tests.
Test all nginx routing configurations through actual HTTP requests.
"""
import pytest
import asyncio
import aiohttp
from typing import Dict, Any
from tests.clients.nginx_test_client import NginxTestClient
class TestNginxRouting:
"""Test nginx reverse proxy routing configuration"""
BASE_URL = "http://localhost:3001" # Test nginx proxy
@pytest.fixture
async def nginx_client(self):
"""Nginx test client"""
return NginxTestClient(self.BASE_URL)
@pytest.fixture
async def http_session(self):
"""HTTP session for nginx testing"""
async with aiohttp.ClientSession() as session:
yield session
@pytest.mark.asyncio
async def test_public_api_routing(self, nginx_client):
"""Test that /api/ routes to public API endpoints"""
results = await nginx_client.test_public_api_routes()
# Verify each route
models_result = results.get("/api/v1/models")
assert models_result is not None
assert models_result["expects_auth"] == True
assert models_result["unauthenticated"]["status_code"] == 401
chat_result = results.get("/api/v1/chat/completions")
assert chat_result is not None
assert chat_result["expects_auth"] == True
assert chat_result["unauthenticated"]["status_code"] == 401
# Health check should not require auth
health_result = results.get("/api/v1/health")
if health_result: # Health endpoint might not exist
assert health_result["expects_auth"] == False or health_result["unauthenticated"]["status_code"] == 200
@pytest.mark.asyncio
async def test_internal_api_routing(self, nginx_client):
"""Test that /api-internal/ routes to internal API endpoints"""
results = await nginx_client.test_internal_api_routes()
# All internal routes should require authentication
for path, result in results.items():
assert result["requires_auth"] == True, f"Internal route {path} should require authentication"
assert result["unauthenticated"]["status_code"] == 401, f"Internal route {path} should return 401 without auth"
@pytest.mark.asyncio
async def test_frontend_routing(self, nginx_client):
"""Test that frontend routes are properly served"""
results = await nginx_client.test_frontend_routes()
# Root path should serve HTML
root_result = results.get("/")
assert root_result is not None
assert root_result["status_code"] in [200, 404] # 404 is acceptable if Next.js not running
# Other frontend routes should at least attempt to serve content
for path, result in results.items():
if path != "/": # Root might have different behavior
assert result["status_code"] in [200, 404, 500], f"Frontend route {path} returned unexpected status {result['status_code']}"
@pytest.mark.asyncio
async def test_cors_headers(self, nginx_client):
"""Test CORS headers are properly set by nginx"""
cors_results = await nginx_client.test_cors_headers()
# Test preflight response
preflight = cors_results.get("preflight", {})
if preflight.get("status_code") == 204: # Successful preflight
cors_headers = preflight.get("cors_headers", {})
assert "access-control-allow-origin" in cors_headers
assert "access-control-allow-methods" in cors_headers
assert "access-control-allow-headers" in cors_headers
# Test actual request CORS headers
request = cors_results.get("request", {})
cors_headers = request.get("cors_headers", {})
# Should have at least allow-origin header
assert len(cors_headers) > 0 or request.get("status_code") == 401 # Auth might block before CORS
@pytest.mark.asyncio
async def test_websocket_support(self, nginx_client):
"""Test that nginx supports WebSocket upgrades for Next.js HMR"""
ws_result = await nginx_client.test_websocket_support()
# Should either upgrade or handle gracefully
assert ws_result["status_code"] in [101, 200, 404, 426], f"Unexpected WebSocket response: {ws_result['status_code']}"
# If upgrade attempted, check headers
if ws_result["upgrade_attempted"]:
assert "upgrade" in ws_result.get("upgrade_header", "").lower() or \
"websocket" in ws_result.get("connection_header", "").lower()
@pytest.mark.asyncio
async def test_health_endpoints(self, nginx_client):
"""Test health check endpoints"""
health_results = await nginx_client.test_health_endpoints()
# At least one health endpoint should be working
healthy_endpoints = [endpoint for endpoint, result in health_results.items() if result["healthy"]]
assert len(healthy_endpoints) > 0, "No health endpoints are responding correctly"
# Test-specific endpoint should work
test_status = health_results.get("/test-status")
if test_status:
assert test_status["healthy"], "Test status endpoint should be working"
@pytest.mark.asyncio
async def test_static_file_caching(self, nginx_client):
"""Test that static files have proper caching headers"""
static_results = await nginx_client.test_static_file_handling()
# Check that caching is configured (even if files don't exist)
# This tests the nginx configuration, not file existence
for file_path, result in static_results.items():
if result["status_code"] == 200: # Only check if file was served
assert result["cached"], f"Static file {file_path} should have cache headers"
@pytest.mark.asyncio
async def test_error_handling(self, nginx_client):
"""Test nginx error handling"""
error_results = await nginx_client.test_error_handling()
for path, result in error_results.items():
# Should return appropriate error status
assert result["actual_status"] >= 400, f"Error path {path} should return error status"
# 404 errors should be handled properly
if result["expected_status"] == 404:
assert result["actual_status"] in [404, 500], f"404 path {path} returned {result['actual_status']}"
@pytest.mark.asyncio
async def test_request_routing_headers(self, http_session):
"""Test that nginx passes correct headers to backend"""
headers_to_test = {
"X-Real-IP": "127.0.0.1",
"X-Forwarded-For": "127.0.0.1",
"User-Agent": "test-client/1.0"
}
# Test header forwarding on API endpoint
async with http_session.get(
f"{self.BASE_URL}/api/v1/models",
headers=headers_to_test
) as response:
# Even if auth fails (401), headers should be forwarded
assert response.status in [401, 200, 422], f"Unexpected status for header test: {response.status}"
@pytest.mark.asyncio
async def test_request_size_limits(self, http_session):
"""Test request size handling"""
# Test large request body
large_payload = {"data": "x" * 1024 * 1024} # 1MB payload
async with http_session.post(
f"{self.BASE_URL}/api/v1/chat/completions",
json=large_payload
) as response:
# Should either handle large request or reject with appropriate status
assert response.status in [401, 413, 400, 422], f"Large request returned {response.status}"
@pytest.mark.asyncio
async def test_concurrent_requests(self, nginx_client):
"""Test nginx handling of concurrent requests"""
load_results = await nginx_client.test_load_balancing(20) # 20 concurrent requests
assert load_results["total_requests"] == 20
assert load_results["failure_rate"] < 0.5, f"High failure rate: {load_results['failure_rate']}"
assert load_results["average_response_time"] < 5.0, f"Slow response time: {load_results['average_response_time']}s"
@pytest.mark.asyncio
async def test_timeout_handling(self, http_session):
"""Test nginx timeout configuration"""
# Test with a custom timeout header to simulate slow backend
timeout = aiohttp.ClientTimeout(total=5)
async with aiohttp.ClientSession(timeout=timeout) as session:
try:
async with session.get(f"{self.BASE_URL}/health") as response:
assert response.status in [200, 401, 404, 500, 502, 503, 504]
except asyncio.TimeoutError:
# Acceptable if nginx has shorter timeout than our client
pass
@pytest.mark.asyncio
async def test_comprehensive_routing(self, nginx_client):
"""Run comprehensive nginx routing test"""
comprehensive_results = await nginx_client.run_comprehensive_test()
# Verify critical components are working
assert "public_api_routes" in comprehensive_results
assert "internal_api_routes" in comprehensive_results
assert "health_endpoints" in comprehensive_results
# At least 50% of tested features should be working correctly
working_features = 0
total_features = 0
for feature_name, feature_results in comprehensive_results.items():
if isinstance(feature_results, dict):
total_features += 1
if self._is_feature_working(feature_name, feature_results):
working_features += 1
success_rate = working_features / total_features if total_features > 0 else 0
assert success_rate >= 0.5, f"Only {success_rate:.1%} of nginx features working"
def _is_feature_working(self, feature_name: str, results: Dict[str, Any]) -> bool:
"""Check if a feature is working based on test results"""
if feature_name == "health_endpoints":
return any(result.get("healthy", False) for result in results.values())
elif feature_name == "load_test":
return results.get("failure_rate", 1.0) < 0.5
elif feature_name in ["public_api_routes", "internal_api_routes"]:
return any(
result.get("requires_auth") or result.get("expects_auth")
for result in results.values()
)
elif feature_name == "cors_headers":
preflight = results.get("preflight", {})
return preflight.get("status_code") in [204, 200] or len(preflight.get("cors_headers", {})) > 0
# Default: consider working if no major errors
return True