From 39c07dd3cf7d08ffb2bd69d8f5e91ac3a2674e6e Mon Sep 17 00:00:00 2001 From: JobvAlewijk Date: Tue, 18 Nov 2025 12:35:44 +0100 Subject: [PATCH] refactor: made pydantic check the input. no longer by the code itself. ref: N25B-198 --- .../api/v1/endpoints/program.py | 16 ++----- .../api/endpoints/test_program_endpoint.py | 42 ++++++++----------- 2 files changed, 21 insertions(+), 37 deletions(-) diff --git a/src/control_backend/api/v1/endpoints/program.py b/src/control_backend/api/v1/endpoints/program.py index e9812ea..a0679d0 100644 --- a/src/control_backend/api/v1/endpoints/program.py +++ b/src/control_backend/api/v1/endpoints/program.py @@ -1,9 +1,7 @@ import logging -from fastapi import APIRouter, HTTPException, Request -from pydantic import ValidationError +from fastapi import APIRouter, Request -from control_backend.schemas.message import Message from control_backend.schemas.program import Program logger = logging.getLogger(__name__) @@ -11,20 +9,12 @@ router = APIRouter() @router.post("/program", status_code=202) -async def receive_message(program: Message, request: Request): +async def receive_message(program: Program, request: Request): """ - Receives a BehaviorProgram as a stringified JSON list inside `message`. + Receives a BehaviorProgram, pydantic checks it. Converts it into real Phase objects. """ logger.debug("Received raw program: %s", program) - raw_str = program.message # This is the JSON string - - # Validate program - try: - program = Program.model_validate_json(raw_str) - except ValidationError as e: - logger.error("Failed to validate program JSON: %s", e) - raise HTTPException(status_code=400, detail="Not a valid program") from None # send away topic = b"program" diff --git a/test/integration/api/endpoints/test_program_endpoint.py b/test/integration/api/endpoints/test_program_endpoint.py index 05ce63c..f6bb261 100644 --- a/test/integration/api/endpoints/test_program_endpoint.py +++ b/test/integration/api/endpoints/test_program_endpoint.py @@ -6,7 +6,6 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from control_backend.api.v1.endpoints import program -from control_backend.schemas.message import Message from control_backend.schemas.program import Program @@ -57,51 +56,48 @@ def test_receive_program_success(client): client.app.state.endpoints_pub_socket = mock_pub_socket program_dict = make_valid_program_dict() - message_body = json.dumps(program_dict) - msg = Message(message=message_body) - # Act - response = client.post("/program", json=msg.model_dump()) + response = client.post("/program", json=program_dict) - # Assert assert response.status_code == 202 assert response.json() == {"status": "Program parsed"} - # Verify socket call (don't compare raw JSON string) + # Verify socket call mock_pub_socket.send_multipart.assert_awaited_once() args, kwargs = mock_pub_socket.send_multipart.await_args assert args[0][0] == b"program" + sent_bytes = args[0][1] - - # Decode sent bytes and compare actual structures sent_obj = json.loads(sent_bytes.decode()) - expected_obj = Program.model_validate_json(message_body).model_dump() + expected_obj = Program.model_validate(program_dict).model_dump() assert sent_obj == expected_obj def test_receive_program_invalid_json(client): - """Invalid JSON string (not parseable) should trigger HTTP 400.""" + """ + Invalid JSON (malformed) -> FastAPI never calls endpoint. + It returns a 422 Unprocessable Entity. + """ mock_pub_socket = AsyncMock() client.app.state.endpoints_pub_socket = mock_pub_socket - bad_json_str = "{invalid json}" - msg = Message(message=bad_json_str) + # FastAPI only accepts valid JSON bodies, so send raw string + response = client.post("/program", content="{invalid json}") - response = client.post("/program", json=msg.model_dump()) - - assert response.status_code == 400 - assert response.json()["detail"] == "Not a valid program" + assert response.status_code == 422 mock_pub_socket.send_multipart.assert_not_called() def test_receive_program_invalid_deep_structure(client): - """Valid JSON shape but invalid deep nested data should still raise 400.""" + """ + Valid JSON but schema invalid -> Pydantic throws validation error -> 422. + """ mock_pub_socket = AsyncMock() client.app.state.endpoints_pub_socket = mock_pub_socket - # Structurally correct Program, but with missing elements + # Missing "value" in norms element bad_program = { "phases": [ { @@ -110,7 +106,7 @@ def test_receive_program_invalid_deep_structure(client): "nextPhaseId": "phase2", "phaseData": { "norms": [ - {"id": "n1", "name": "norm"} # Missing "value" + {"id": "n1", "name": "norm"} # INVALID: missing "value" ], "goals": [ {"id": "g1", "name": "goal", "description": "desc", "achieved": False} @@ -123,9 +119,7 @@ def test_receive_program_invalid_deep_structure(client): ] } - msg = Message(message=json.dumps(bad_program)) - response = client.post("/program", json=msg.model_dump()) + response = client.post("/program", json=bad_program) - assert response.status_code == 400 - assert response.json()["detail"] == "Not a valid program" + assert response.status_code == 422 mock_pub_socket.send_multipart.assert_not_called()