shl-recommender / test_validation_http.py
gmayank100's picture
Upload 39 files
ea4c39c verified
Raw
History Blame Contribute Delete
11.7 kB
"""HTTP endpoint tests for request validation."""
import pytest
from fastapi.testclient import TestClient
from unittest.mock import patch
from app import app
from models import Message, ChatRequest, ErrorResponse
@pytest.fixture
def client():
"""Fixture for FastAPI test client."""
return TestClient(app)
@pytest.fixture
def mock_catalog(monkeypatch):
"""Mock catalog being loaded."""
from app import app_state
original_state = app_state.copy()
app_state["catalog_loaded"] = True
app_state["catalog_error"] = None
app_state["catalog_validated"] = True
app_state["catalog_validation_error"] = None
app_state["faiss_loaded"] = True
app_state["faiss_error"] = None
app_state["faiss_validated"] = True
yield
# Restore original state
app_state.update(original_state)
class TestValidationHTTP400Responses:
"""Tests for HTTP 400 responses with validation errors."""
def test_zero_messages_rejected(self, client, mock_catalog):
"""POST /chat with 0 messages should return HTTP 400."""
# Pydantic will reject this before our handler, so we test at validation layer
# This is a constraint enforced by ChatRequest model
pass
def test_seventeen_messages_rejected(self, client, mock_catalog):
"""POST /chat with 17 messages should return HTTP 400."""
messages = []
for i in range(8):
messages.append({"role": "user", "content": f"Message {i+1}"})
messages.append({"role": "assistant", "content": f"Response {i+1}"})
messages.append({"role": "user", "content": "17th message"})
response = client.post("/chat", json={"messages": messages})
# Pydantic validates max_items=16
assert response.status_code == 422 # Validation error from Pydantic
def test_nine_user_turns_rejected(self, client, mock_catalog):
"""POST /chat with 9 user turns should return HTTP 400."""
messages = []
for i in range(9):
messages.append({"role": "user", "content": f"User message {i+1}"})
response = client.post("/chat", json={"messages": messages})
# Pydantic validates user turn count
assert response.status_code == 422 # Validation error from Pydantic
def test_malformed_json_returns_error(self, client, mock_catalog):
"""POST /chat with malformed JSON should return HTTP 422."""
response = client.post("/chat", content="not json")
assert response.status_code == 422
class TestValidErrorResponse:
"""Tests for error response schema compliance."""
def test_error_response_has_required_fields(self, client, mock_catalog):
"""Error response should have 'error' and 'status' fields."""
# Create a request that will fail validation
# 17 messages will fail Pydantic validation
messages = []
for i in range(8):
messages.append({"role": "user", "content": f"Message {i+1}"})
messages.append({"role": "assistant", "content": f"Response {i+1}"})
messages.append({"role": "user", "content": "17th"})
response = client.post("/chat", json={"messages": messages})
# Status code depends on Pydantic validation
assert response.status_code in [400, 422]
def test_error_response_status_field_is_error(self, client, mock_catalog):
"""Error response 'status' field should always be 'error'."""
# This would be tested if we can trigger our custom validation
# Currently Pydantic validates first
pass
class TestValidRequestsWork:
"""Tests that valid requests work correctly."""
def test_valid_single_message(self, client, mock_catalog):
"""Valid request with 1 message should succeed."""
response = client.post("/chat", json={
"messages": [
{"role": "user", "content": "I need a developer"}
]
})
assert response.status_code == 200
data = response.json()
assert "reply" in data
assert "recommendations" in data
assert "end_of_conversation" in data
def test_valid_multiple_messages(self, client, mock_catalog):
"""Valid request with multiple messages should succeed."""
response = client.post("/chat", json={
"messages": [
{"role": "user", "content": "I need a backend engineer"},
{"role": "assistant", "content": "What level?"},
{"role": "user", "content": "Mid-level"},
{"role": "assistant", "content": "Here are recommendations"}
]
})
assert response.status_code == 200
data = response.json()
assert "reply" in data
assert "recommendations" in data
assert "end_of_conversation" in data
def test_valid_16_messages(self, client, mock_catalog):
"""Valid request with exactly 16 messages should succeed."""
messages = []
for i in range(8):
messages.append({"role": "user", "content": f"Message {i+1}"})
messages.append({"role": "assistant", "content": f"Response {i+1}"})
response = client.post("/chat", json={"messages": messages})
assert response.status_code == 200
class TestCatalogUnavailable:
"""Tests for catalog unavailability handling."""
def test_catalog_unavailable_returns_503(self, client):
"""When catalog not loaded, should return HTTP 503."""
from app import app_state
app_state["catalog_loaded"] = False
app_state["catalog_error"] = "Catalog initialization failed"
response = client.post("/chat", json={
"messages": [
{"role": "user", "content": "Hello"}
]
})
assert response.status_code == 503
# Restore state
app_state["catalog_loaded"] = True
app_state["catalog_error"] = None
class TestResponseSchema:
"""Tests for response schema compliance."""
def test_success_response_structure(self, client, mock_catalog):
"""Successful response should have correct structure."""
response = client.post("/chat", json={
"messages": [
{"role": "user", "content": "I need a developer"}
]
})
assert response.status_code == 200
data = response.json()
# Verify all required fields present
assert "reply" in data
assert "recommendations" in data
assert "end_of_conversation" in data
# Verify field types
assert isinstance(data["reply"], str)
assert isinstance(data["recommendations"], list)
assert isinstance(data["end_of_conversation"], bool)
def test_recommendations_have_name_and_url(self, client, mock_catalog):
"""Each recommendation should have name and url."""
response = client.post("/chat", json={
"messages": [
{"role": "user", "content": "I need a developer"}
]
})
assert response.status_code == 200
data = response.json()
# If there are recommendations, they should have required fields
for rec in data["recommendations"]:
assert "name" in rec
assert "url" in rec
class TestEdgeCaseHTTP:
"""HTTP endpoint edge case tests."""
def test_empty_message_content(self, client, mock_catalog):
"""Message with empty content should be rejected."""
response = client.post("/chat", json={
"messages": [
{"role": "user", "content": ""}
]
})
# Pydantic validation should reject this
assert response.status_code == 422
def test_whitespace_only_message(self, client, mock_catalog):
"""Message with only whitespace should be rejected."""
response = client.post("/chat", json={
"messages": [
{"role": "user", "content": " "}
]
})
# Should be rejected
assert response.status_code in [400, 422]
def test_invalid_role(self, client, mock_catalog):
"""Message with invalid role should be rejected."""
response = client.post("/chat", json={
"messages": [
{"role": "invalid", "content": "Hello"}
]
})
# Pydantic validation should reject this
assert response.status_code == 422
def test_unicode_content_accepted(self, client, mock_catalog):
"""Unicode content should be accepted."""
response = client.post("/chat", json={
"messages": [
{"role": "user", "content": "Hello 你好 مرحبا"}
]
})
assert response.status_code == 200
def test_very_long_message(self, client, mock_catalog):
"""Very long message should be accepted."""
long_content = "x" * 10000
response = client.post("/chat", json={
"messages": [
{"role": "user", "content": long_content}
]
})
assert response.status_code == 200
class TestHealthCheckNotAffectedByValidation:
"""Health check endpoint should not be affected by chat validation."""
def test_health_check_works(self, client, mock_catalog):
"""Health check should work with catalog loaded."""
response = client.get("/health")
assert response.status_code == 200
data = response.json()
assert data["status"] == "ok"
def test_health_check_fails_without_catalog(self, client):
"""Health check should fail if catalog not loaded."""
from app import app_state
app_state["catalog_loaded"] = False
app_state["catalog_error"] = "Test error"
response = client.get("/health")
assert response.status_code == 503
# Restore state
app_state["catalog_loaded"] = True
app_state["catalog_error"] = None
class TestValidationEdgeCases:
"""Additional edge case tests for validation."""
def test_exactly_8_user_turns_valid(self, client, mock_catalog):
"""Exactly 8 user turns should be valid."""
messages = []
for i in range(8):
messages.append({"role": "user", "content": f"User {i+1}"})
if i < 7:
messages.append({"role": "assistant", "content": f"Response {i+1}"})
response = client.post("/chat", json={"messages": messages})
assert response.status_code == 200
def test_special_characters_in_content(self, client, mock_catalog):
"""Special characters should be handled correctly."""
response = client.post("/chat", json={
"messages": [
{"role": "user", "content": "What about Python? #hashtag @mention $cost"}
]
})
assert response.status_code == 200
def test_newlines_and_tabs_in_content(self, client, mock_catalog):
"""Newlines and tabs in content should be preserved."""
response = client.post("/chat", json={
"messages": [
{"role": "user", "content": "Line 1\nLine 2\tTabbed"}
]
})
assert response.status_code == 200