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.message import Message 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() message_body = json.dumps(program_dict) msg = Message(message=message_body) # Act response = client.post("/program", json=msg.model_dump()) # Assert assert response.status_code == 202 assert response.json() == {"status": "Program parsed"} # Verify socket call (don't compare raw JSON string) 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() assert sent_obj == expected_obj def test_receive_program_invalid_json(client): """Invalid JSON string (not parseable) should trigger HTTP 400.""" mock_pub_socket = AsyncMock() client.app.state.endpoints_pub_socket = mock_pub_socket bad_json_str = "{invalid json}" msg = Message(message=bad_json_str) response = client.post("/program", json=msg.model_dump()) assert response.status_code == 400 assert response.json()["detail"] == "Not a valid program" 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.""" mock_pub_socket = AsyncMock() client.app.state.endpoints_pub_socket = mock_pub_socket # Structurally correct Program, but with missing elements bad_program = { "phases": [ { "id": "phase1", "name": "deepfail", "nextPhaseId": "phase2", "phaseData": { "norms": [ {"id": "n1", "name": "norm"} # Missing "value" ], "goals": [ {"id": "g1", "name": "goal", "description": "desc", "achieved": False} ], "triggers": [ {"id": "t1", "label": "trigger", "type": "keyword", "value": ["start"]} ], }, } ] } msg = Message(message=json.dumps(bad_program)) response = client.post("/program", json=msg.model_dump()) assert response.status_code == 400 assert response.json()["detail"] == "Not a valid program" mock_pub_socket.send_multipart.assert_not_called()