refactor: made pydantic check the input.

no longer by the code itself.

ref: N25B-198
This commit is contained in:
JobvAlewijk
2025-11-18 12:35:44 +01:00
parent 2ed2a84f13
commit 39c07dd3cf
2 changed files with 21 additions and 37 deletions

View File

@@ -1,9 +1,7 @@
import logging import logging
from fastapi import APIRouter, HTTPException, Request from fastapi import APIRouter, Request
from pydantic import ValidationError
from control_backend.schemas.message import Message
from control_backend.schemas.program import Program from control_backend.schemas.program import Program
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -11,20 +9,12 @@ router = APIRouter()
@router.post("/program", status_code=202) @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. Converts it into real Phase objects.
""" """
logger.debug("Received raw program: %s", program) 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 # send away
topic = b"program" topic = b"program"

View File

@@ -6,7 +6,6 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from control_backend.api.v1.endpoints import program from control_backend.api.v1.endpoints import program
from control_backend.schemas.message import Message
from control_backend.schemas.program import Program 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 client.app.state.endpoints_pub_socket = mock_pub_socket
program_dict = make_valid_program_dict() program_dict = make_valid_program_dict()
message_body = json.dumps(program_dict)
msg = Message(message=message_body)
# Act response = client.post("/program", json=program_dict)
response = client.post("/program", json=msg.model_dump())
# Assert
assert response.status_code == 202 assert response.status_code == 202
assert response.json() == {"status": "Program parsed"} 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() mock_pub_socket.send_multipart.assert_awaited_once()
args, kwargs = mock_pub_socket.send_multipart.await_args args, kwargs = mock_pub_socket.send_multipart.await_args
assert args[0][0] == b"program" assert args[0][0] == b"program"
sent_bytes = args[0][1] sent_bytes = args[0][1]
# Decode sent bytes and compare actual structures
sent_obj = json.loads(sent_bytes.decode()) 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 assert sent_obj == expected_obj
def test_receive_program_invalid_json(client): 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() mock_pub_socket = AsyncMock()
client.app.state.endpoints_pub_socket = mock_pub_socket client.app.state.endpoints_pub_socket = mock_pub_socket
bad_json_str = "{invalid json}" # FastAPI only accepts valid JSON bodies, so send raw string
msg = Message(message=bad_json_str) response = client.post("/program", content="{invalid json}")
response = client.post("/program", json=msg.model_dump()) assert response.status_code == 422
assert response.status_code == 400
assert response.json()["detail"] == "Not a valid program"
mock_pub_socket.send_multipart.assert_not_called() mock_pub_socket.send_multipart.assert_not_called()
def test_receive_program_invalid_deep_structure(client): 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() mock_pub_socket = AsyncMock()
client.app.state.endpoints_pub_socket = mock_pub_socket client.app.state.endpoints_pub_socket = mock_pub_socket
# Structurally correct Program, but with missing elements # Missing "value" in norms element
bad_program = { bad_program = {
"phases": [ "phases": [
{ {
@@ -110,7 +106,7 @@ def test_receive_program_invalid_deep_structure(client):
"nextPhaseId": "phase2", "nextPhaseId": "phase2",
"phaseData": { "phaseData": {
"norms": [ "norms": [
{"id": "n1", "name": "norm"} # Missing "value" {"id": "n1", "name": "norm"} # INVALID: missing "value"
], ],
"goals": [ "goals": [
{"id": "g1", "name": "goal", "description": "desc", "achieved": False} {"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=bad_program)
response = client.post("/program", json=msg.model_dump())
assert response.status_code == 400 assert response.status_code == 422
assert response.json()["detail"] == "Not a valid program"
mock_pub_socket.send_multipart.assert_not_called() mock_pub_socket.send_multipart.assert_not_called()