373 lines
14 KiB
Python
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
|