""" 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