"""Test unified error response schema. This test suite verifies that all error responses use the unified ErrorResponse format with code, detail, metadata, and correlation_id fields. This ensures: 1. Frontend can handle all errors with a single schema. 2. Error codes are machine-readable. 3. Error details include structured metadata. """ from __future__ import annotations from typing import Any import pytest from httpx import ASGITransport, AsyncClient from pydantic import BaseModel from app.config import Settings from app.main import create_app class RequestModel(BaseModel): """Test request model with validation.""" name: str age: int class TestErrorResponseSchema: """Verify all error responses use the unified ErrorResponse schema.""" @pytest.mark.asyncio async def test_validation_error_returns_unified_schema( self, test_settings: Settings ) -> None: """RequestValidationError returns unified ErrorResponse format.""" app = create_app(test_settings) transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: @app.post("/test-validation") async def test_validation(body: RequestModel) -> dict[str, Any]: return {"ok": True} # Send invalid data that violates validation response = await client.post( "/test-validation", json={"name": "John", "age": "not_a_number"} ) assert response.status_code == 400 data = response.json() # Verify unified schema assert "code" in data, "Missing 'code' field" assert "detail" in data, "Missing 'detail' field" assert "metadata" in data, "Missing 'metadata' field" assert "correlation_id" in data, "Missing 'correlation_id' field" # Verify values assert data["code"] == "invalid_input" assert "validation" in data["detail"].lower() assert isinstance(data["metadata"], dict) @pytest.mark.asyncio async def test_validation_error_includes_field_details( self, test_settings: Settings ) -> None: """Validation error metadata includes field error count and first field.""" app = create_app(test_settings) transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: @app.post("/test-validation") async def test_validation(body: RequestModel) -> dict[str, Any]: return {"ok": True} response = await client.post( "/test-validation", json={"name": "John", "age": "invalid"} ) assert response.status_code == 400 data = response.json() # Verify metadata contains field error information assert "field_errors" in data["metadata"] assert "first_field" in data["metadata"] assert data["metadata"]["field_errors"] > 0 assert "age" in data["metadata"]["first_field"] @pytest.mark.asyncio async def test_validation_error_has_correlation_id( self, test_settings: Settings ) -> None: """Validation errors include correlation_id for tracing.""" app = create_app(test_settings) transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: @app.post("/test-validation") async def test_validation(body: RequestModel) -> dict[str, Any]: return {"ok": True} response = await client.post( "/test-validation", json={"name": 123, "age": "not_a_number"} ) assert response.status_code == 400 data = response.json() # correlation_id can be None but field should exist assert "correlation_id" in data @pytest.mark.asyncio async def test_missing_required_field_validation( self, test_settings: Settings ) -> None: """Missing required fields return validation error.""" app = create_app(test_settings) transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: @app.post("/test-validation") async def test_validation(body: RequestModel) -> dict[str, Any]: return {"ok": True} # Send incomplete data response = await client.post("/test-validation", json={"name": "John"}) assert response.status_code == 400 data = response.json() assert data["code"] == "invalid_input" assert "validation" in data["detail"].lower() assert "field_errors" in data["metadata"] assert data["metadata"]["field_errors"] > 0 @pytest.mark.asyncio async def test_validation_error_response_structure(self, test_settings: Settings) -> None: """All validation errors return consistent ErrorResponse structure.""" app = create_app(test_settings) transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: @app.post("/test-validation") async def test_validation(body: RequestModel) -> dict[str, Any]: return {"ok": True} # Test with wrong type response = await client.post( "/test-validation", json={"name": "John", "age": []}, ) assert response.status_code == 400 data = response.json() assert data["code"] == "invalid_input" assert "metadata" in data assert isinstance(data["metadata"], dict) @pytest.mark.asyncio async def test_validation_error_does_not_leak_sensitive_data( self, test_settings: Settings ) -> None: """Validation errors do not leak internal implementation details.""" app = create_app(test_settings) transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: @app.post("/test-validation") async def test_validation(body: RequestModel) -> dict[str, Any]: return {"ok": True} response = await client.post( "/test-validation", json={"name": "John", "age": "not_a_number"} ) assert response.status_code == 400 data = response.json() # Verify the response structure is safe assert "stack" not in data assert "traceback" not in data assert "exc_info" not in data