""" Tests for validation error utilities. Tests the format_validation_error() and handle_validation_error() functions that transform Pydantic validation errors into user-friendly messages. """ from django.test import TestCase, tag import json from pydantic import BaseModel, ValidationError, field_validator from typing import Literal from ninja.errors import HttpError from epapi.utils.validation_errors import format_validation_error, handle_validation_error @tag("api", "utils") class ValidationErrorUtilityTests(TestCase): """Test validation error utility functions.""" def test_format_missing_field_error(self): """Test formatting of missing required field error.""" # Create a model with required field class TestModel(BaseModel): required_field: str # Trigger validation error try: TestModel() except ValidationError as e: errors = e.errors() self.assertEqual(len(errors), 1) formatted = format_validation_error(errors[0]) self.assertEqual(formatted, "This field is required") def test_format_enum_error(self): """Test formatting of enum validation error.""" class TestModel(BaseModel): status: Literal["active", "inactive"] try: TestModel(status="invalid") except ValidationError as e: errors = e.errors() self.assertEqual(len(errors), 1) formatted = format_validation_error(errors[0]) # Literal errors get formatted as "Please enter ..." with the valid options self.assertIn("Please enter", formatted) self.assertIn("active", formatted) self.assertIn("inactive", formatted) def test_format_string_type_error(self): """Test formatting of string type validation error.""" class TestModel(BaseModel): name: str try: TestModel(name=123) except ValidationError as e: errors = e.errors() self.assertEqual(len(errors), 1) formatted = format_validation_error(errors[0]) self.assertEqual(formatted, "Please enter a valid string") def test_format_int_type_error(self): """Test formatting of integer type validation error.""" class TestModel(BaseModel): count: int try: TestModel(count="not_a_number") except ValidationError as e: errors = e.errors() self.assertEqual(len(errors), 1) formatted = format_validation_error(errors[0]) self.assertEqual(formatted, "Please enter a valid int") def test_format_float_type_error(self): """Test formatting of float type validation error.""" class TestModel(BaseModel): value: float try: TestModel(value="not_a_float") except ValidationError as e: errors = e.errors() self.assertEqual(len(errors), 1) formatted = format_validation_error(errors[0]) self.assertEqual(formatted, "Please enter a valid float") def test_format_value_error(self): """Test formatting of value error from custom validator.""" class TestModel(BaseModel): age: int @field_validator("age") @classmethod def validate_age(cls, v): if v < 0: raise ValueError("Age must be positive") return v try: TestModel(age=-5) except ValidationError as e: errors = e.errors() self.assertEqual(len(errors), 1) formatted = format_validation_error(errors[0]) self.assertEqual(formatted, "Age must be positive") def test_handle_validation_error_structure(self): """Test that handle_validation_error raises HttpError with correct structure.""" class TestModel(BaseModel): name: str count: int try: TestModel(name=123, count="invalid") except ValidationError as e: # handle_validation_error should raise HttpError with self.assertRaises(HttpError) as context: handle_validation_error(e) http_error = context.exception self.assertEqual(http_error.status_code, 400) # Parse the JSON from the error message error_data = json.loads(http_error.message) # Check structure self.assertEqual(error_data["type"], "validation_error") self.assertIn("field_errors", error_data) self.assertIn("message", error_data) self.assertEqual(error_data["message"], "Please correct the errors below") # Check that both fields have errors self.assertIn("name", error_data["field_errors"]) self.assertIn("count", error_data["field_errors"]) def test_handle_validation_error_no_pydantic_internals(self): """Test that handle_validation_error doesn't expose Pydantic internals.""" class TestModel(BaseModel): email: str try: TestModel(email=123) except ValidationError as e: with self.assertRaises(HttpError) as context: handle_validation_error(e) http_error = context.exception error_data = json.loads(http_error.message) error_str = json.dumps(error_data) # Ensure no Pydantic internals are exposed self.assertNotIn("pydantic", error_str.lower()) self.assertNotIn("https://errors.pydantic.dev", error_str) self.assertNotIn("loc", error_str) def test_handle_validation_error_user_friendly_messages(self): """Test that all error messages are user-friendly.""" class TestModel(BaseModel): name: str age: int status: Literal["active", "inactive"] try: TestModel(name=123, status="invalid") # Multiple errors except ValidationError as e: with self.assertRaises(HttpError) as context: handle_validation_error(e) http_error = context.exception error_data = json.loads(http_error.message) # All messages should be user-friendly (contain "Please" or "This field") for field, messages in error_data["field_errors"].items(): for message in messages: # User-friendly messages start with "Please" or "This field" self.assertTrue( message.startswith("Please") or message.startswith("This field"), f"Message '{message}' is not user-friendly", ) def test_handle_validation_error_multiple_errors_same_field(self): """Test handling multiple validation errors for the same field.""" class TestModel(BaseModel): value: int @field_validator("value") @classmethod def validate_range(cls, v): if v < 0: raise ValueError("Must be non-negative") if v > 100: raise ValueError("Must be at most 100") return v # Test with string (type error) - this will fail before the validator runs try: TestModel(value="invalid") except ValidationError as e: with self.assertRaises(HttpError) as context: handle_validation_error(e) http_error = context.exception error_data = json.loads(http_error.message) # Should have error for 'value' field self.assertIn("value", error_data["field_errors"]) self.assertIsInstance(error_data["field_errors"]["value"], list) self.assertGreater(len(error_data["field_errors"]["value"]), 0)