Files
catonline_ai/vw-agentic-rag/tests/integration/test_api.py
2025-09-26 17:15:54 +08:00

373 lines
14 KiB
Python

"""
Remote Integration Tests for Agentic RAG API
These tests connect to a running service instance remotely to validate:
- API endpoints and responses
- Request/response schemas
- Basic functionality without external dependencies
"""
import pytest
import asyncio
import json
import httpx
from typing import Optional, Dict, Any
import time
import os
# Configuration for remote service connection
DEFAULT_SERVICE_URL = "http://127.0.0.1:8000"
SERVICE_URL = os.getenv("AGENTIC_RAG_SERVICE_URL", DEFAULT_SERVICE_URL)
@pytest.fixture(scope="session")
def service_url() -> str:
"""Get the service URL for testing"""
return SERVICE_URL
class TestBasicAPI:
"""Test basic API endpoints and functionality"""
@pytest.mark.asyncio
async def test_health_endpoint(self, service_url: str):
"""Test service health endpoint"""
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(f"{service_url}/health")
assert response.status_code == 200
data = response.json()
assert data["status"] == "healthy"
assert data["service"] == "agentic-rag"
@pytest.mark.asyncio
async def test_root_endpoint(self, service_url: str):
"""Test root API endpoint"""
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(f"{service_url}/")
assert response.status_code == 200
data = response.json()
assert "message" in data
assert "Agentic RAG API" in data["message"]
@pytest.mark.asyncio
async def test_openapi_docs(self, service_url: str):
"""Test OpenAPI documentation endpoint"""
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(f"{service_url}/openapi.json")
assert response.status_code == 200
data = response.json()
assert "openapi" in data
assert "info" in data
assert data["info"]["title"] == "Agentic RAG API"
@pytest.mark.asyncio
async def test_docs_endpoint(self, service_url: str):
"""Test Swagger UI docs endpoint"""
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(f"{service_url}/docs")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
class TestChatAPI:
"""Test chat API endpoints with valid requests"""
def _create_chat_request(self, message: str, session_id: Optional[str] = None) -> Dict[str, Any]:
"""Create a valid chat request"""
return {
"session_id": session_id or f"test_session_{int(time.time())}",
"messages": [
{
"role": "user",
"content": message
}
]
}
@pytest.mark.asyncio
async def test_chat_endpoint_basic_request(self, service_url: str):
"""Test basic chat endpoint request/response structure"""
request_data = self._create_chat_request("Hello, can you help me?")
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
f"{service_url}/api/chat",
json=request_data,
headers={"Content-Type": "application/json"}
)
assert response.status_code == 200
# Response should be streaming text/event-stream
assert "text/event-stream" in response.headers.get("content-type", "") or \
"text/plain" in response.headers.get("content-type", "")
@pytest.mark.asyncio
async def test_ai_sdk_chat_endpoint_basic_request(self, service_url: str):
"""Test AI SDK compatible chat endpoint"""
request_data = self._create_chat_request("What is ISO 26262?")
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
f"{service_url}/api/ai-sdk/chat",
json=request_data,
headers={"Content-Type": "application/json"}
)
assert response.status_code == 200
# AI SDK endpoint returns plain text stream
assert "text/plain" in response.headers.get("content-type", "")
@pytest.mark.asyncio
async def test_chat_endpoint_invalid_request(self, service_url: str):
"""Test chat endpoint with invalid request data"""
invalid_requests = [
{}, # Empty request
{"session_id": "test"}, # Missing messages
{"messages": []}, # Missing session_id
{"session_id": "test", "messages": [{"role": "invalid"}]}, # Invalid message format
]
async with httpx.AsyncClient(timeout=30.0) as client:
for invalid_request in invalid_requests:
response = await client.post(
f"{service_url}/api/chat",
json=invalid_request,
headers={"Content-Type": "application/json"}
)
# Should return 422 for validation errors
assert response.status_code == 422
@pytest.mark.asyncio
async def test_session_persistence(self, service_url: str):
"""Test that sessions persist across multiple requests"""
session_id = f"persistent_session_{int(time.time())}"
async with httpx.AsyncClient(timeout=30.0) as client:
# First message
request1 = self._create_chat_request("My name is John", session_id)
response1 = await client.post(
f"{service_url}/api/chat",
json=request1,
headers={"Content-Type": "application/json"}
)
assert response1.status_code == 200
# Wait a moment for processing
await asyncio.sleep(1)
# Second message referring to previous context
request2 = self._create_chat_request("What did I just tell you my name was?", session_id)
response2 = await client.post(
f"{service_url}/api/chat",
json=request2,
headers={"Content-Type": "application/json"}
)
assert response2.status_code == 200
class TestRequestValidation:
"""Test request validation and error handling"""
@pytest.mark.asyncio
async def test_malformed_json(self, service_url: str):
"""Test endpoint with malformed JSON"""
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
f"{service_url}/api/chat",
content="invalid json{",
headers={"Content-Type": "application/json"}
)
assert response.status_code == 422
@pytest.mark.asyncio
async def test_missing_content_type(self, service_url: str):
"""Test endpoint without proper content type"""
request_data = {
"session_id": "test_session",
"messages": [{"role": "user", "content": "test"}]
}
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
f"{service_url}/api/chat",
content=json.dumps(request_data)
# No Content-Type header
)
# FastAPI should handle this gracefully
assert response.status_code in [415, 422]
@pytest.mark.asyncio
async def test_oversized_request(self, service_url: str):
"""Test endpoint with very large request"""
large_content = "x" * 100000 # 100KB message
request_data = {
"session_id": "test_session",
"messages": [{"role": "user", "content": large_content}]
}
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
f"{service_url}/api/chat",
json=request_data,
headers={"Content-Type": "application/json"}
)
# Should either process or reject gracefully
assert response.status_code in [200, 413, 422]
class TestCORSAndHeaders:
"""Test CORS and security headers"""
@pytest.mark.asyncio
async def test_cors_headers(self, service_url: str):
"""Test CORS headers are properly set"""
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.options(
f"{service_url}/api/chat",
headers={
"Origin": "http://localhost:3000",
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "Content-Type"
}
)
# CORS preflight should be handled
assert response.status_code in [200, 204]
# Check for CORS headers in actual request
request_data = {
"session_id": "cors_test",
"messages": [{"role": "user", "content": "test"}]
}
response = await client.post(
f"{service_url}/api/chat",
json=request_data,
headers={
"Content-Type": "application/json",
"Origin": "http://localhost:3000"
}
)
assert response.status_code == 200
# Should have CORS headers
assert "access-control-allow-origin" in response.headers
@pytest.mark.asyncio
async def test_security_headers(self, service_url: str):
"""Test basic security headers"""
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(f"{service_url}/health")
assert response.status_code == 200
# Check for basic security practices
# Note: Specific headers depend on deployment configuration
headers = response.headers
# FastAPI should include some basic headers
assert "content-length" in headers or "transfer-encoding" in headers
class TestErrorHandling:
"""Test error handling and edge cases"""
@pytest.mark.asyncio
async def test_nonexistent_endpoint(self, service_url: str):
"""Test request to non-existent endpoint"""
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(f"{service_url}/nonexistent")
assert response.status_code == 404
@pytest.mark.asyncio
async def test_method_not_allowed(self, service_url: str):
"""Test wrong HTTP method on endpoint"""
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(f"{service_url}/api/chat") # GET instead of POST
assert response.status_code == 405
@pytest.mark.asyncio
async def test_timeout_handling(self, service_url: str):
"""Test request timeout handling"""
# Use a very short timeout to test timeout handling
async with httpx.AsyncClient(timeout=0.001) as short_timeout_client:
try:
response = await short_timeout_client.get(f"{service_url}/health")
# If it doesn't timeout, that's also fine
assert response.status_code == 200
except httpx.TimeoutException:
# Expected timeout - this is fine
pass
class TestServiceIntegration:
"""Test integration with actual service features"""
@pytest.mark.asyncio
async def test_manufacturing_standards_query(self, service_url: str):
"""Test query related to manufacturing standards"""
request_data = {
"session_id": f"standards_test_{int(time.time())}",
"messages": [
{
"role": "user",
"content": "What are the key safety requirements in ISO 26262?"
}
]
}
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(
f"{service_url}/api/ai-sdk/chat",
json=request_data,
headers={"Content-Type": "application/json"}
)
assert response.status_code == 200
# Read some of the streaming response
content = ""
async for chunk in response.aiter_text():
content += chunk
if len(content) > 100: # Read enough to verify it's working
break
# Should have some content indicating it's processing
assert len(content) > 0
@pytest.mark.asyncio
async def test_general_conversation(self, service_url: str):
"""Test general conversation capability"""
request_data = {
"session_id": f"general_test_{int(time.time())}",
"messages": [
{
"role": "user",
"content": "Hello! How can you help me today?"
}
]
}
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(
f"{service_url}/api/chat",
json=request_data,
headers={"Content-Type": "application/json"}
)
assert response.status_code == 200
# Verify we get streaming response
content = ""
chunk_count = 0
async for chunk in response.aiter_text():
content += chunk
chunk_count += 1
if chunk_count > 10: # Read several chunks
break
# Should receive streaming content
assert len(content) > 0