Spaces:
Running
Running
| """ | |
| 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) | |
| 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) | |