From 6cc03efdaf911b39763f1e28698fff55a26bf21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Thu, 6 Nov 2025 14:42:02 +0100 Subject: [PATCH] feat: new integration tests for robot, making sure to get 100% code coverage ref: N25B-256 --- src/control_backend/api/v1/endpoints/robot.py | 1 - .../api/endpoints/test_robot_endpoint.py | 97 ++++++++++++++++++- 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/src/control_backend/api/v1/endpoints/robot.py b/src/control_backend/api/v1/endpoints/robot.py index 96e32ac..dfa7332 100644 --- a/src/control_backend/api/v1/endpoints/robot.py +++ b/src/control_backend/api/v1/endpoints/robot.py @@ -41,7 +41,6 @@ async def ping_stream(request: Request): # Set up internal socket to receive ping updates logger.debug("Ping stream router event stream entered.") - # TODO: Check with Kasper sub_socket = Context.instance().socket(zmq.SUB) sub_socket.connect(settings.zmq_settings.internal_sub_address) sub_socket.setsockopt(zmq.SUBSCRIBE, b"ping") diff --git a/test/integration/api/endpoints/test_robot_endpoint.py b/test/integration/api/endpoints/test_robot_endpoint.py index 3a2df88..0f71951 100644 --- a/test/integration/api/endpoints/test_robot_endpoint.py +++ b/test/integration/api/endpoints/test_robot_endpoint.py @@ -1,4 +1,5 @@ -from unittest.mock import AsyncMock +import json +from unittest.mock import AsyncMock, MagicMock import pytest from fastapi import FastAPI @@ -59,3 +60,97 @@ def test_receive_command_invalid_payload(client): bad_payload = {"invalid": "data"} response = client.post("/command", json=bad_payload) assert response.status_code == 422 # validation error + + +def test_ping_check_returns_none(client): + """Ensure /ping_check returns 200 and None (currently unimplemented).""" + response = client.get("/ping_check") + assert response.status_code == 200 + assert response.json() is None + + +@pytest.mark.asyncio +async def test_ping_stream_yields_ping_event(monkeypatch): + """Test that ping_stream yields a proper SSE message when a ping is received.""" + mock_sub_socket = AsyncMock() + mock_sub_socket.connect = MagicMock() + mock_sub_socket.setsockopt = MagicMock() + mock_sub_socket.recv_multipart = AsyncMock(return_value=[b"ping", b"true"]) + + mock_context = MagicMock() + mock_context.socket.return_value = mock_sub_socket + monkeypatch.setattr(robot.Context, "instance", lambda: mock_context) + + mock_request = AsyncMock() + mock_request.is_disconnected = AsyncMock(side_effect=[False, True]) + + response = await robot.ping_stream(mock_request) + generator = aiter(response.body_iterator) + + event = await anext(generator) + event_text = event.decode() if isinstance(event, bytes) else str(event) + assert event_text.strip() == "data: true" + + with pytest.raises(StopAsyncIteration): + await anext(generator) + + mock_sub_socket.connect.assert_called_once() + mock_sub_socket.setsockopt.assert_called_once_with(robot.zmq.SUBSCRIBE, b"ping") + mock_sub_socket.recv_multipart.assert_awaited() + + +@pytest.mark.asyncio +async def test_ping_stream_handles_timeout(monkeypatch): + """Test that ping_stream continues looping on TimeoutError.""" + mock_sub_socket = AsyncMock() + mock_sub_socket.connect = MagicMock() + mock_sub_socket.setsockopt = MagicMock() + mock_sub_socket.recv_multipart.side_effect = TimeoutError() + + mock_context = MagicMock() + mock_context.socket.return_value = mock_sub_socket + monkeypatch.setattr(robot.Context, "instance", lambda: mock_context) + + mock_request = AsyncMock() + mock_request.is_disconnected = AsyncMock(return_value=True) + + response = await robot.ping_stream(mock_request) + generator = aiter(response.body_iterator) + + with pytest.raises(StopAsyncIteration): + await anext(generator) + + mock_sub_socket.connect.assert_called_once() + mock_sub_socket.setsockopt.assert_called_once_with(robot.zmq.SUBSCRIBE, b"ping") + mock_sub_socket.recv_multipart.assert_awaited() + + +@pytest.mark.asyncio +async def test_ping_stream_yields_json_values(monkeypatch): + """Ensure ping_stream correctly parses and yields JSON body values.""" + mock_sub_socket = AsyncMock() + mock_sub_socket.connect = MagicMock() + mock_sub_socket.setsockopt = MagicMock() + mock_sub_socket.recv_multipart = AsyncMock( + return_value=[b"ping", json.dumps({"connected": True}).encode()] + ) + + mock_context = MagicMock() + mock_context.socket.return_value = mock_sub_socket + monkeypatch.setattr(robot.Context, "instance", lambda: mock_context) + + mock_request = AsyncMock() + mock_request.is_disconnected = AsyncMock(side_effect=[False, True]) + + response = await robot.ping_stream(mock_request) + generator = aiter(response.body_iterator) + + event = await anext(generator) + event_text = event.decode() if isinstance(event, bytes) else str(event) + + assert "connected" in event_text + assert "true" in event_text + + mock_sub_socket.connect.assert_called_once() + mock_sub_socket.setsockopt.assert_called_once_with(robot.zmq.SUBSCRIBE, b"ping") + mock_sub_socket.recv_multipart.assert_awaited()