""" Test REST API validation error handling and responses. """ import json import pytest from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from fastapi.testclient import TestClient from pydantic import BaseModel, Field, ValidationError from typing import Optional, List # Import just the REST API router, not the full app from rest_api import ( build_router, _format_validation_error, DesignRequest, ) # Create a minimal FastAPI app with just the REST router api = FastAPI() api.include_router(build_router()) # Add custom validation error handler (same as in app.py) @api.exception_handler(RequestValidationError) async def validation_exception_handler( request, exc: RequestValidationError, ) -> JSONResponse: """Handle Pydantic validation errors with user-friendly messages.""" return JSONResponse( status_code=422, content=_format_validation_error(exc), ) client = TestClient(api) class TestValidationErrors: """Test that validation errors are user-friendly.""" def test_missing_crop_name_defaults_to_generic(self): """Test that missing crop_name defaults to generic and succeeds.""" payload = { "farm": { "name": "Test Farm", "location": { "address": "123 Main St", "latitude": 19.999, "longitude": 73.793 }, "size": { "value": 456, "unit": "hectare" } # Missing crop_name should default to generic }, "plots": [ { "plot_id": "plot-1", "name": "Plot 1", "boundaries": [ {"latitude": 19.291, "longitude": 73.654}, {"latitude": 19.289, "longitude": 73.654}, {"latitude": 19.292, "longitude": 73.659} ] } ], "water_sources": [ { "water_source_id": "ws-1", "type": "Motor", "name": "Water Source 1", "location": {"latitude": 19.257, "longitude": 73.706} } ] } response = client.post("/rest/v1/design", json=payload) # Should succeed assert response.status_code == 200 data = response.json() # Check successful response structure assert "design_summary" in data assert "bom" in data # Verify that design summary is present (the main output) ds = data.get("design_summary", {}) assert ds # Should have design summary assert ds.get("farm_area_ha") # Should have calculated farm area def test_missing_plots_error(self): """Test that missing plots array returns clear error.""" payload = { "farm": { "name": "Test Farm", "location": { "address": "123 Main St", "latitude": 19.999, "longitude": 73.793 }, "crop_name": "Tomato" }, # Missing plots "water_sources": [ { "water_source_id": "ws-1", "type": "Motor", "name": "Water Source 1", "location": {"latitude": 19.257, "longitude": 73.706} } ] } response = client.post("/rest/v1/design", json=payload) assert response.status_code == 422 data = response.json() assert data["status"] == "validation_error" error_messages = [err["message"] for err in data["errors"]] assert any("plot" in msg.lower() for msg in error_messages), \ f"Expected plots-related error, got: {error_messages}" def test_missing_water_sources_error(self): """Test that missing water sources returns clear error.""" payload = { "farm": { "name": "Test Farm", "location": { "address": "123 Main St", "latitude": 19.999, "longitude": 73.793 }, "crop_name": "Tomato" }, "plots": [ { "plot_id": "plot-1", "name": "Plot 1", "boundaries": [ {"latitude": 19.291, "longitude": 73.654}, {"latitude": 19.289, "longitude": 73.654}, {"latitude": 19.292, "longitude": 73.659} ] } ] # Missing water_sources } response = client.post("/rest/v1/design", json=payload) assert response.status_code == 422 data = response.json() assert data["status"] == "validation_error" error_messages = [err["message"] for err in data["errors"]] assert any("water_source" in msg.lower() or "pump" in msg.lower() for msg in error_messages), \ f"Expected water_sources-related error, got: {error_messages}" def test_valid_request_succeeds(self): """Test that a valid request succeeds.""" payload = { "farm": { "name": "Test Farm", "location": { "address": "123 Main St", "latitude": 19.999, "longitude": 73.793 }, "size": { "value": 10, "unit": "acre" }, "crop_name": "Tomato" }, "plots": [ { "plot_id": "plot-1", "name": "Plot 1", "boundaries": [ {"latitude": 19.291, "longitude": 73.654}, {"latitude": 19.289, "longitude": 73.654}, {"latitude": 19.292, "longitude": 73.659} ] } ], "water_sources": [ { "water_source_id": "ws-1", "type": "Motor", "name": "Water Source 1", "location": {"latitude": 19.257, "longitude": 73.706} } ], "design_type": "centralized" } response = client.post("/rest/v1/design", json=payload) # Should succeed assert response.status_code == 200 data = response.json() # Check response structure assert "design_summary" in data assert "bom" in data assert "geojson" in data def test_error_response_format(self): """Test that error response follows expected format.""" payload = { "farm": { "name": "Test Farm", "location": { "address": "123 Main St", "latitude": 19.999, "longitude": 73.793 } # Missing crop_name }, "plots": [ { "plot_id": "plot-1", "boundaries": [ {"latitude": 19.291, "longitude": 73.654}, {"latitude": 19.289, "longitude": 73.654}, {"latitude": 19.292, "longitude": 73.659} ] } ], # Missing water_sources } response = client.post("/rest/v1/design", json=payload) data = response.json() # Verify error structure assert data["status"] == "validation_error" assert isinstance(data["errors"], list) for error in data["errors"]: assert "field" in error, "Error should have 'field' key" assert "message" in error, "Error should have 'message' key" assert isinstance(error["field"], str) assert isinstance(error["message"], str)