| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- """Route tests for /scheduled-dryings (#2638)."""
- from datetime import datetime, timedelta, timezone
- from unittest.mock import patch
- import pytest
- def _future_iso(hours: int = 2) -> str:
- return (datetime.now(timezone.utc) + timedelta(hours=hours)).isoformat()
- @pytest.mark.asyncio
- async def test_create_and_list(async_client, printer_factory):
- printer = await printer_factory()
- resp = await async_client.post(
- "/api/v1/scheduled-dryings",
- json={"printer_id": printer.id, "ams_id": 0, "temp": 65, "duration_hours": 8, "start_after": _future_iso()},
- )
- assert resp.status_code == 200, resp.text
- body = resp.json()
- assert body["status"] == "pending"
- assert body["printer_id"] == printer.id
- listed = await async_client.get(f"/api/v1/scheduled-dryings?printer_id={printer.id}")
- assert listed.status_code == 200
- assert [row["id"] for row in listed.json()] == [body["id"]]
- @pytest.mark.asyncio
- async def test_create_rejects_past_start_after(async_client, printer_factory):
- printer = await printer_factory()
- past = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
- resp = await async_client.post(
- "/api/v1/scheduled-dryings",
- json={"printer_id": printer.id, "temp": 65, "duration_hours": 8, "start_after": past},
- )
- assert resp.status_code == 400
- @pytest.mark.asyncio
- async def test_create_unknown_printer_404(async_client):
- resp = await async_client.post(
- "/api/v1/scheduled-dryings", json={"printer_id": 99999, "temp": 65, "duration_hours": 8}
- )
- assert resp.status_code == 404
- @pytest.mark.asyncio
- async def test_create_invalid_temp_422(async_client, printer_factory):
- printer = await printer_factory()
- resp = await async_client.post(
- "/api/v1/scheduled-dryings", json={"printer_id": printer.id, "temp": 90, "duration_hours": 8}
- )
- assert resp.status_code == 422
- @pytest.mark.asyncio
- async def test_cancel_pending(async_client, printer_factory):
- printer = await printer_factory()
- created = await async_client.post(
- "/api/v1/scheduled-dryings",
- json={"printer_id": printer.id, "temp": 65, "duration_hours": 8, "start_after": _future_iso()},
- )
- row_id = created.json()["id"]
- resp = await async_client.delete(f"/api/v1/scheduled-dryings/{row_id}")
- assert resp.status_code == 200
- assert resp.json()["status"] == "cancelled"
- # Cancelled rows disappear from the active list
- listed = await async_client.get(f"/api/v1/scheduled-dryings?printer_id={printer.id}")
- assert listed.json() == []
- # Second cancel is a 400 (not active any more)
- resp = await async_client.delete(f"/api/v1/scheduled-dryings/{row_id}")
- assert resp.status_code == 400
- @pytest.mark.asyncio
- async def test_cancel_running_sends_stop_command(async_client, printer_factory, db_session):
- from backend.app.models.scheduled_drying import ScheduledDrying
- printer = await printer_factory()
- row = ScheduledDrying(printer_id=printer.id, ams_id=1, temp=65, duration_hours=8, status="running")
- db_session.add(row)
- await db_session.commit()
- await db_session.refresh(row)
- with patch("backend.app.api.routes.scheduled_dryings.printer_manager") as mock_pm:
- mock_pm.send_drying_command.return_value = True
- resp = await async_client.delete(f"/api/v1/scheduled-dryings/{row.id}")
- assert resp.status_code == 200
- mock_pm.send_drying_command.assert_called_once_with(printer.id, 1, 0, 0, mode=0)
- @pytest.mark.asyncio
- async def test_screen_only_model_rejected_at_schedule_time(async_client, printer_factory):
- """A P1S can never run a scheduled dry, so say so in the UI now."""
- printer = await printer_factory(model="P1S")
- resp = await async_client.post(
- "/api/v1/scheduled-dryings",
- json={"printer_id": printer.id, "temp": 65, "duration_hours": 8, "start_after": _future_iso()},
- )
- assert resp.status_code == 400
- assert "screen" in resp.json()["detail"].lower()
- @pytest.mark.asyncio
- async def test_offline_printer_is_still_schedulable(async_client, printer_factory):
- """Firmware is unreadable while offline and may be upgraded before the run."""
- printer = await printer_factory()
- resp = await async_client.post(
- "/api/v1/scheduled-dryings",
- json={"printer_id": printer.id, "temp": 65, "duration_hours": 8, "start_after": _future_iso()},
- )
- assert resp.status_code == 200, resp.text
- @pytest.mark.asyncio
- async def test_stale_firmware_rejected_when_printer_is_online(async_client, printer_factory):
- printer = await printer_factory()
- state = type("S", (), {"firmware_version": "01.05.00.00", "raw_data": {}})()
- with patch("backend.app.api.routes.scheduled_dryings.printer_manager") as mock_pm:
- mock_pm.get_status.return_value = state
- resp = await async_client.post(
- "/api/v1/scheduled-dryings",
- json={"printer_id": printer.id, "temp": 65, "duration_hours": 8, "start_after": _future_iso()},
- )
- assert resp.status_code == 400
- @pytest.mark.asyncio
- async def test_timestamps_carry_z_suffix(async_client, printer_factory):
- """Matches every print_queue route, which the frontend parses the same way."""
- printer = await printer_factory()
- resp = await async_client.post(
- "/api/v1/scheduled-dryings",
- json={"printer_id": printer.id, "temp": 65, "duration_hours": 8, "start_after": _future_iso()},
- )
- body = resp.json()
- assert body["start_after"].endswith("Z")
- assert body["created_at"].endswith("Z")
- @pytest.mark.asyncio
- async def test_failed_rows_are_listed_and_dismissable(async_client, printer_factory, db_session):
- """A run that only fails at dispatch has to reach the client, not just the log."""
- from backend.app.models.scheduled_drying import ScheduledDrying
- printer = await printer_factory()
- row = ScheduledDrying(
- printer_id=printer.id,
- temp=65,
- duration_hours=8,
- status="failed",
- error_message="Drying not supported for this printer model or firmware version",
- )
- db_session.add(row)
- await db_session.commit()
- await db_session.refresh(row)
- listed = await async_client.get(f"/api/v1/scheduled-dryings?printer_id={printer.id}")
- assert [r["id"] for r in listed.json()] == [row.id]
- assert listed.json()[0]["error_message"].startswith("Drying not supported")
- resp = await async_client.delete(f"/api/v1/scheduled-dryings/{row.id}")
- assert resp.status_code == 200
- assert resp.json()["status"] == "dismissed"
- assert (await async_client.get(f"/api/v1/scheduled-dryings?printer_id={printer.id}")).json() == []
- # Dismissing twice is a 404, not a second dismiss: the row is gone.
- assert (await async_client.delete(f"/api/v1/scheduled-dryings/{row.id}")).status_code == 404
- @pytest.mark.asyncio
- async def test_list_orders_by_start_after_then_id(async_client, printer_factory):
- printer = await printer_factory()
- later = await async_client.post(
- "/api/v1/scheduled-dryings",
- json={"printer_id": printer.id, "temp": 65, "duration_hours": 8, "start_after": _future_iso(5)},
- )
- sooner = await async_client.post(
- "/api/v1/scheduled-dryings",
- json={"printer_id": printer.id, "temp": 65, "duration_hours": 8, "start_after": _future_iso(2)},
- )
- listed = await async_client.get(f"/api/v1/scheduled-dryings?printer_id={printer.id}")
- assert [r["id"] for r in listed.json()] == [sooner.json()["id"], later.json()["id"]]
- @pytest.mark.asyncio
- async def test_over_long_filament_422(async_client, printer_factory):
- printer = await printer_factory()
- resp = await async_client.post(
- "/api/v1/scheduled-dryings",
- json={"printer_id": printer.id, "temp": 65, "duration_hours": 8, "filament": "X" * 51},
- )
- assert resp.status_code == 422
|