""" Unit tests for API response formatting. Tests JSON construction and source reference inclusion in API responses. **Validates: Requirements 6.1, 6.2, 6.3, 6.4, 6.5** """ import pytest from pydantic import ValidationError from src.main import QuestionRequest, QuestionResponse class TestQuestionRequestValidation: """Test cases for QuestionRequest model validation.""" def test_valid_request(self): """Should accept valid question.""" request = QuestionRequest(question="How do I reset my password?") assert request.question == "How do I reset my password?" def test_reject_empty_question(self): """Should reject empty question string.""" with pytest.raises(ValidationError) as exc_info: QuestionRequest(question="") assert "question" in str(exc_info.value).lower() def test_reject_whitespace_only(self): """Should reject whitespace-only question.""" with pytest.raises(ValidationError) as exc_info: QuestionRequest(question=" \n\t ") assert "question" in str(exc_info.value).lower() def test_reject_missing_question(self): """Should reject request without question field.""" with pytest.raises(ValidationError): QuestionRequest() def test_reject_too_long_question(self): """Should reject question exceeding max length.""" long_question = "a" * 501 # Max is 500 with pytest.raises(ValidationError) as exc_info: QuestionRequest(question=long_question) assert "500" in str(exc_info.value) or "length" in str(exc_info.value).lower() def test_trim_whitespace(self): """Should trim leading/trailing whitespace from question.""" request = QuestionRequest(question=" How do I reset? ") assert request.question == "How do I reset?" assert not request.question.startswith(" ") assert not request.question.endswith(" ") def test_accept_max_length_question(self): """Should accept question at exactly max length.""" question = "a" * 500 request = QuestionRequest(question=question) assert len(request.question) == 500 class TestQuestionResponseConstruction: """Test cases for QuestionResponse model construction.""" def test_answer_response_construction(self): """Should construct valid answer response.""" response = QuestionResponse( response_type="answer", message="Here is your answer.", confidence_explanation="High confidence based on retrieval.", sources=["billing.md"] ) assert response.response_type == "answer" assert response.message == "Here is your answer." assert response.confidence_explanation == "High confidence based on retrieval." assert response.sources == ["billing.md"] def test_escalation_response_construction(self): """Should construct valid escalation response.""" response = QuestionResponse( response_type="escalation", message="Let me connect you with a human agent.", confidence_explanation="Low confidence - score below threshold.", sources=None ) assert response.response_type == "escalation" assert response.message == "Let me connect you with a human agent." assert response.confidence_explanation == "Low confidence - score below threshold." assert response.sources is None def test_sources_optional(self): """Sources field should be optional.""" response = QuestionResponse( response_type="answer", message="Answer", confidence_explanation="Explanation" ) assert response.sources is None def test_reject_invalid_response_type(self): """Should reject invalid response_type values.""" with pytest.raises(ValidationError) as exc_info: QuestionResponse( response_type="invalid", message="Message", confidence_explanation="Explanation" ) assert "response_type" in str(exc_info.value).lower() def test_reject_missing_required_fields(self): """Should reject response missing required fields.""" with pytest.raises(ValidationError): QuestionResponse(response_type="answer") def test_multiple_sources(self): """Should accept multiple source references.""" response = QuestionResponse( response_type="answer", message="Answer", confidence_explanation="Explanation", sources=["billing.md", "permissions.md", "troubleshooting.md"] ) assert len(response.sources) == 3 assert "billing.md" in response.sources def test_empty_sources_list(self): """Should accept empty sources list.""" response = QuestionResponse( response_type="answer", message="Answer", confidence_explanation="Explanation", sources=[] ) assert response.sources == [] class TestResponseJSONSerialization: """Test cases for JSON serialization of responses.""" def test_answer_json_schema(self): """Answer response should serialize to correct JSON schema.""" response = QuestionResponse( response_type="answer", message="Your answer here.", confidence_explanation="High confidence.", sources=["billing.md"] ) json_data = response.model_dump() assert "response_type" in json_data assert "message" in json_data assert "confidence_explanation" in json_data assert "sources" in json_data assert json_data["response_type"] == "answer" assert json_data["message"] == "Your answer here." assert json_data["confidence_explanation"] == "High confidence." assert json_data["sources"] == ["billing.md"] def test_escalation_json_schema(self): """Escalation response should serialize to correct JSON schema.""" response = QuestionResponse( response_type="escalation", message="Connecting you with an agent.", confidence_explanation="Low confidence.", sources=None ) json_data = response.model_dump() assert json_data["response_type"] == "escalation" assert json_data["message"] == "Connecting you with an agent." assert json_data["confidence_explanation"] == "Low confidence." assert json_data["sources"] is None def test_json_field_types(self): """JSON fields should have correct types.""" response = QuestionResponse( response_type="answer", message="Answer", confidence_explanation="Explanation", sources=["test.md"] ) json_data = response.model_dump() assert isinstance(json_data["response_type"], str) assert isinstance(json_data["message"], str) assert isinstance(json_data["confidence_explanation"], str) assert isinstance(json_data["sources"], list) def test_json_roundtrip(self): """Should be able to serialize and deserialize.""" original = QuestionResponse( response_type="answer", message="Test answer", confidence_explanation="Test explanation", sources=["test.md"] ) # Serialize to dict json_data = original.model_dump() # Deserialize back restored = QuestionResponse(**json_data) assert restored.response_type == original.response_type assert restored.message == original.message assert restored.confidence_explanation == original.confidence_explanation assert restored.sources == original.sources class TestResponseFieldContent: """Test cases for response field content validation.""" def test_message_not_empty(self): """Message field should not be empty.""" # Pydantic doesn't enforce non-empty strings by default, # but we can test that empty strings are technically allowed # (business logic should prevent this) response = QuestionResponse( response_type="answer", message="", confidence_explanation="Explanation" ) assert response.message == "" def test_confidence_explanation_not_empty(self): """Confidence explanation should not be empty.""" response = QuestionResponse( response_type="answer", message="Message", confidence_explanation="" ) assert response.confidence_explanation == "" def test_response_type_literal_values(self): """Response type should only accept 'answer' or 'escalation'.""" # Valid values answer_response = QuestionResponse( response_type="answer", message="M", confidence_explanation="E" ) assert answer_response.response_type == "answer" escalation_response = QuestionResponse( response_type="escalation", message="M", confidence_explanation="E" ) assert escalation_response.response_type == "escalation" # Invalid value should raise error with pytest.raises(ValidationError): QuestionResponse( response_type="unknown", message="M", confidence_explanation="E" ) class TestSourceReferenceFormatting: """Test cases for source reference formatting.""" def test_sources_as_list_of_strings(self): """Sources should be a list of strings.""" response = QuestionResponse( response_type="answer", message="Answer", confidence_explanation="Explanation", sources=["billing.md", "permissions.md"] ) assert isinstance(response.sources, list) assert all(isinstance(s, str) for s in response.sources) def test_sources_preserve_order(self): """Sources list should preserve order.""" sources = ["c.md", "a.md", "b.md"] response = QuestionResponse( response_type="answer", message="Answer", confidence_explanation="Explanation", sources=sources ) assert response.sources == sources def test_sources_allow_duplicates(self): """Sources list should allow duplicates (though business logic may prevent this).""" response = QuestionResponse( response_type="answer", message="Answer", confidence_explanation="Explanation", sources=["test.md", "test.md"] ) assert len(response.sources) == 2 def test_escalation_typically_has_no_sources(self): """Escalation responses typically have no sources.""" response = QuestionResponse( response_type="escalation", message="Escalating", confidence_explanation="Low confidence", sources=None ) assert response.sources is None def test_answer_can_have_sources(self): """Answer responses can include sources.""" response = QuestionResponse( response_type="answer", message="Answer", confidence_explanation="High confidence", sources=["billing.md"] ) assert response.sources is not None assert len(response.sources) > 0 class TestResponseConsistency: """Test cases for response schema consistency.""" def test_all_responses_have_required_fields(self): """All responses must have response_type, message, and confidence_explanation.""" # Answer response answer = QuestionResponse( response_type="answer", message="A", confidence_explanation="E" ) assert hasattr(answer, "response_type") assert hasattr(answer, "message") assert hasattr(answer, "confidence_explanation") assert hasattr(answer, "sources") # Escalation response escalation = QuestionResponse( response_type="escalation", message="A", confidence_explanation="E" ) assert hasattr(escalation, "response_type") assert hasattr(escalation, "message") assert hasattr(escalation, "confidence_explanation") assert hasattr(escalation, "sources") def test_response_type_determines_content(self): """Response type should indicate whether it's an answer or escalation.""" answer = QuestionResponse( response_type="answer", message="Here is your answer", confidence_explanation="High confidence" ) escalation = QuestionResponse( response_type="escalation", message="Let me connect you with an agent", confidence_explanation="Low confidence" ) assert answer.response_type == "answer" assert escalation.response_type == "escalation" assert answer.response_type != escalation.response_type