diff --git a/test/unit/api/v1/endpoints/test_program_endpoint.py b/test/unit/api/v1/endpoints/test_program_endpoint.py new file mode 100644 index 0000000..f6bb261 --- /dev/null +++ b/test/unit/api/v1/endpoints/test_program_endpoint.py @@ -0,0 +1,125 @@ +import json +from unittest.mock import AsyncMock + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from control_backend.api.v1.endpoints import program +from control_backend.schemas.program import Program + + +@pytest.fixture +def app(): + """Create a FastAPI app with the /program route and mock socket.""" + app = FastAPI() + app.include_router(program.router) + return app + + +@pytest.fixture +def client(app): + """Create a TestClient.""" + return TestClient(app) + + +def make_valid_program_dict(): + """Helper to create a valid Program JSON structure.""" + return { + "phases": [ + { + "id": "phase1", + "name": "basephase", + "nextPhaseId": "phase2", + "phaseData": { + "norms": [{"id": "n1", "name": "norm", "value": "be nice"}], + "goals": [ + {"id": "g1", "name": "goal", "description": "test goal", "achieved": False} + ], + "triggers": [ + { + "id": "t1", + "label": "trigger", + "type": "keyword", + "value": ["stop", "exit"], + } + ], + }, + } + ] + } + + +def test_receive_program_success(client): + """Valid Program JSON should be parsed and sent through the socket.""" + mock_pub_socket = AsyncMock() + client.app.state.endpoints_pub_socket = mock_pub_socket + + program_dict = make_valid_program_dict() + + response = client.post("/program", json=program_dict) + + assert response.status_code == 202 + assert response.json() == {"status": "Program parsed"} + + # 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] + sent_obj = json.loads(sent_bytes.decode()) + + expected_obj = Program.model_validate(program_dict).model_dump() + assert sent_obj == expected_obj + + +def test_receive_program_invalid_json(client): + """ + 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 + + # FastAPI only accepts valid JSON bodies, so send raw string + response = client.post("/program", content="{invalid json}") + + assert response.status_code == 422 + mock_pub_socket.send_multipart.assert_not_called() + + +def test_receive_program_invalid_deep_structure(client): + """ + Valid JSON but schema invalid -> Pydantic throws validation error -> 422. + """ + mock_pub_socket = AsyncMock() + client.app.state.endpoints_pub_socket = mock_pub_socket + + # Missing "value" in norms element + bad_program = { + "phases": [ + { + "id": "phase1", + "name": "deepfail", + "nextPhaseId": "phase2", + "phaseData": { + "norms": [ + {"id": "n1", "name": "norm"} # INVALID: missing "value" + ], + "goals": [ + {"id": "g1", "name": "goal", "description": "desc", "achieved": False} + ], + "triggers": [ + {"id": "t1", "label": "trigger", "type": "keyword", "value": ["start"]} + ], + }, + } + ] + } + + response = client.post("/program", json=bad_program) + + assert response.status_code == 422 + mock_pub_socket.send_multipart.assert_not_called()