init
This commit is contained in:
372
vw-agentic-rag/tests/integration/test_api.py
Normal file
372
vw-agentic-rag/tests/integration/test_api.py
Normal file
@@ -0,0 +1,372 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user