"""Unit tests for the bed-jog and home-axes endpoints (#791). Tests: POST /api/v1/printers/{printer_id}/bed-jog?distance=&force= POST /api/v1/printers/{printer_id}/home-axes?axes= """ from unittest.mock import MagicMock, patch import pytest from httpx import AsyncClient class TestBedJogAPI: @pytest.mark.asyncio async def test_bed_jog_not_found(self, async_client: AsyncClient): response = await async_client.post("/api/v1/printers/99999/bed-jog?distance=10") assert response.status_code == 404 @pytest.mark.asyncio async def test_bed_jog_zero_distance_rejected(self, async_client: AsyncClient, printer_factory): printer = await printer_factory(name="P1") response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=0") assert response.status_code == 400 assert "distance" in response.json()["detail"].lower() @pytest.mark.asyncio async def test_bed_jog_too_large_rejected(self, async_client: AsyncClient, printer_factory): printer = await printer_factory(name="P1") response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=500") assert response.status_code == 400 @pytest.mark.asyncio async def test_bed_jog_not_connected(self, async_client: AsyncClient, printer_factory): printer = await printer_factory(name="Disconnected") with patch("backend.app.api.routes.printers.printer_manager") as mock_pm: mock_pm.get_client.return_value = None response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=10") assert response.status_code == 400 assert "not connected" in response.json()["detail"].lower() @pytest.mark.asyncio async def test_bed_jog_send_failure(self, async_client: AsyncClient, printer_factory): printer = await printer_factory(name="P1") mock_client = MagicMock() mock_client.send_gcode.return_value = False with patch("backend.app.api.routes.printers.printer_manager") as mock_pm: mock_pm.get_client.return_value = mock_client response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=10") assert response.status_code == 500 @pytest.mark.asyncio async def test_bed_jog_success_without_force(self, async_client: AsyncClient, printer_factory): """When force=false the M211 guard lines must not be emitted.""" printer = await printer_factory(name="P1") mock_client = MagicMock() mock_client.send_gcode.return_value = True with patch("backend.app.api.routes.printers.printer_manager") as mock_pm: mock_pm.get_client.return_value = mock_client response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=10&force=false") assert response.status_code == 200 sent_gcode = mock_client.send_gcode.call_args[0][0] assert "G91" in sent_gcode assert "G1 Z10.00" in sent_gcode assert "G90" in sent_gcode assert "M211" not in sent_gcode @pytest.mark.asyncio async def test_bed_jog_success_with_force(self, async_client: AsyncClient, printer_factory): """force=true must wrap the move in M211 S0 / M211 S1.""" printer = await printer_factory(name="P1") mock_client = MagicMock() mock_client.send_gcode.return_value = True with patch("backend.app.api.routes.printers.printer_manager") as mock_pm: mock_pm.get_client.return_value = mock_client response = await async_client.post(f"/api/v1/printers/{printer.id}/bed-jog?distance=-5&force=true") assert response.status_code == 200 sent_gcode = mock_client.send_gcode.call_args[0][0] lines = sent_gcode.splitlines() assert lines[0] == "M211 S0" assert lines[-1] == "M211 S1" assert "G1 Z-5.00" in sent_gcode class TestHomeAxesAPI: @pytest.mark.asyncio async def test_home_axes_not_found(self, async_client: AsyncClient): response = await async_client.post("/api/v1/printers/99999/home-axes?axes=z") assert response.status_code == 404 @pytest.mark.asyncio async def test_home_axes_invalid(self, async_client: AsyncClient, printer_factory): printer = await printer_factory(name="P1") response = await async_client.post(f"/api/v1/printers/{printer.id}/home-axes?axes=bogus") assert response.status_code == 400 @pytest.mark.asyncio @pytest.mark.parametrize( "axes,expected", [("z", "G28 Z"), ("xy", "G28 X Y"), ("all", "G28")], ) async def test_home_axes_success(self, async_client: AsyncClient, printer_factory, axes, expected): printer = await printer_factory(name="P1") mock_client = MagicMock() mock_client.send_gcode.return_value = True with patch("backend.app.api.routes.printers.printer_manager") as mock_pm: mock_pm.get_client.return_value = mock_client response = await async_client.post(f"/api/v1/printers/{printer.id}/home-axes?axes={axes}") assert response.status_code == 200 mock_client.send_gcode.assert_called_once_with(expected) @pytest.mark.asyncio async def test_home_axes_not_connected(self, async_client: AsyncClient, printer_factory): printer = await printer_factory(name="D") with patch("backend.app.api.routes.printers.printer_manager") as mock_pm: mock_pm.get_client.return_value = None response = await async_client.post(f"/api/v1/printers/{printer.id}/home-axes?axes=z") assert response.status_code == 400