farm-layout-model / test_rest_api.py
spacedout-bits's picture
Make crop_name optional in REST API, default to generic
2c6e6b0
Raw
History Blame Contribute Delete
8.39 kB
"""
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)