| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580 |
- """Integration tests for Printers API endpoints.
- Tests the full request/response cycle for /api/v1/printers/ endpoints.
- """
- from unittest.mock import MagicMock, patch
- import pytest
- from httpx import AsyncClient
- class TestPrintersAPI:
- """Integration tests for /api/v1/printers/ endpoints."""
- # ========================================================================
- # List endpoints
- # ========================================================================
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_list_printers_empty(self, async_client: AsyncClient):
- """Verify empty list is returned when no printers exist."""
- response = await async_client.get("/api/v1/printers/")
- assert response.status_code == 200
- assert response.json() == []
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_list_printers_with_data(self, async_client: AsyncClient, printer_factory, db_session):
- """Verify list returns existing printers."""
- await printer_factory(name="Test Printer")
- response = await async_client.get("/api/v1/printers/")
- assert response.status_code == 200
- data = response.json()
- assert len(data) >= 1
- assert any(p["name"] == "Test Printer" for p in data)
- # ========================================================================
- # Create endpoints
- # ========================================================================
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_create_printer(self, async_client: AsyncClient):
- """Verify printer can be created."""
- data = {
- "name": "New Printer",
- "serial_number": "00M09A111111111",
- "ip_address": "192.168.1.100",
- "access_code": "12345678",
- "is_active": True,
- "model": "X1C",
- }
- response = await async_client.post("/api/v1/printers/", json=data)
- assert response.status_code == 200
- result = response.json()
- assert result["name"] == "New Printer"
- assert result["serial_number"] == "00M09A111111111"
- assert result["model"] == "X1C"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_create_printer_with_hostname(self, async_client: AsyncClient):
- """Verify printer can be created with a hostname instead of IP address."""
- data = {
- "name": "DNS Printer",
- "serial_number": "00M09A555555555",
- "ip_address": "printer.local",
- "access_code": "12345678",
- "model": "P1S",
- }
- response = await async_client.post("/api/v1/printers/", json=data)
- assert response.status_code == 200
- result = response.json()
- assert result["name"] == "DNS Printer"
- assert result["ip_address"] == "printer.local"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_create_printer_with_fqdn(self, async_client: AsyncClient):
- """Verify printer can be created with a fully qualified domain name."""
- data = {
- "name": "FQDN Printer",
- "serial_number": "00M09A666666666",
- "ip_address": "my-printer.home.lan",
- "access_code": "12345678",
- "model": "X1C",
- }
- response = await async_client.post("/api/v1/printers/", json=data)
- assert response.status_code == 200
- result = response.json()
- assert result["ip_address"] == "my-printer.home.lan"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_create_printer_invalid_hostname(self, async_client: AsyncClient):
- """Verify invalid hostnames are rejected."""
- data = {
- "name": "Bad Printer",
- "serial_number": "00M09A777777777",
- "ip_address": "-invalid",
- "access_code": "12345678",
- }
- response = await async_client.post("/api/v1/printers/", json=data)
- assert response.status_code == 422
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_create_printer_duplicate_serial(self, async_client: AsyncClient, printer_factory, db_session):
- """Verify duplicate serial number is rejected."""
- await printer_factory(serial_number="00M09A222222222")
- data = {
- "name": "Duplicate Printer",
- "serial_number": "00M09A222222222",
- "ip_address": "192.168.1.101",
- "access_code": "12345678",
- }
- response = await async_client.post("/api/v1/printers/", json=data)
- # Should fail due to duplicate serial
- assert response.status_code in [400, 409, 422, 500]
- # ========================================================================
- # Get single endpoint
- # ========================================================================
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_printer(self, async_client: AsyncClient, printer_factory, db_session):
- """Verify single printer can be retrieved."""
- printer = await printer_factory(name="Get Test Printer")
- response = await async_client.get(f"/api/v1/printers/{printer.id}")
- assert response.status_code == 200
- result = response.json()
- assert result["id"] == printer.id
- assert result["name"] == "Get Test Printer"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_printer_not_found(self, async_client: AsyncClient):
- """Verify 404 for non-existent printer."""
- response = await async_client.get("/api/v1/printers/9999")
- assert response.status_code == 404
- # ========================================================================
- # Update endpoints
- # ========================================================================
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_update_printer_name(self, async_client: AsyncClient, printer_factory, db_session):
- """Verify printer name can be updated."""
- printer = await printer_factory(name="Original Name")
- response = await async_client.patch(f"/api/v1/printers/{printer.id}", json={"name": "Updated Name"})
- assert response.status_code == 200
- assert response.json()["name"] == "Updated Name"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_update_printer_active_status(self, async_client: AsyncClient, printer_factory, db_session):
- """Verify printer active status can be updated."""
- printer = await printer_factory(is_active=True)
- response = await async_client.patch(f"/api/v1/printers/{printer.id}", json={"is_active": False})
- assert response.status_code == 200
- assert response.json()["is_active"] is False
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_update_printer_auto_archive(self, async_client: AsyncClient, printer_factory, db_session):
- """Verify auto_archive setting can be updated."""
- printer = await printer_factory(auto_archive=True)
- response = await async_client.patch(f"/api/v1/printers/{printer.id}", json={"auto_archive": False})
- assert response.status_code == 200
- assert response.json()["auto_archive"] is False
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_update_nonexistent_printer(self, async_client: AsyncClient):
- """Verify updating non-existent printer returns 404."""
- response = await async_client.patch("/api/v1/printers/9999", json={"name": "New Name"})
- assert response.status_code == 404
- # ========================================================================
- # Delete endpoints
- # ========================================================================
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_delete_printer(self, async_client: AsyncClient, printer_factory, db_session):
- """Verify printer can be deleted."""
- printer = await printer_factory()
- printer_id = printer.id
- response = await async_client.delete(f"/api/v1/printers/{printer_id}")
- assert response.status_code == 200
- # Verify deleted
- response = await async_client.get(f"/api/v1/printers/{printer_id}")
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_delete_nonexistent_printer(self, async_client: AsyncClient):
- """Verify deleting non-existent printer returns 404."""
- response = await async_client.delete("/api/v1/printers/9999")
- assert response.status_code == 404
- # ========================================================================
- # Status endpoint
- # ========================================================================
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_printer_status(
- self, async_client: AsyncClient, printer_factory, mock_printer_manager, db_session
- ):
- """Verify printer status can be retrieved."""
- printer = await printer_factory()
- response = await async_client.get(f"/api/v1/printers/{printer.id}/status")
- assert response.status_code == 200
- result = response.json()
- assert "connected" in result
- assert "state" in result
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_printer_status_not_found(self, async_client: AsyncClient):
- """Verify 404 for status of non-existent printer."""
- response = await async_client.get("/api/v1/printers/9999/status")
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_printer_status_includes_fila_switch_when_installed(
- self, async_client: AsyncClient, printer_factory, db_session
- ):
- """When the FTS accessory is installed, the status response must include
- the fila_switch object with the routing arrays. See #1162.
- The accessory is detected from print.device.fila_switch in MQTT;
- we feed a PrinterState with FilaSwitchState(installed=True, ...) and
- confirm it survives the schema serialization round-trip.
- """
- from unittest.mock import MagicMock, patch
- from backend.app.services.bambu_mqtt import FilaSwitchState, PrinterState
- printer = await printer_factory()
- state = PrinterState()
- state.connected = True
- state.state = "IDLE"
- state.fila_switch = FilaSwitchState(
- installed=True,
- in_slots=[-1, 2],
- out_extruders=[0, 1],
- stat=0,
- info=2,
- )
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_status = MagicMock(return_value=state)
- mock_pm.is_awaiting_plate_clear = MagicMock(return_value=False)
- response = await async_client.get(f"/api/v1/printers/{printer.id}/status")
- assert response.status_code == 200
- result = response.json()
- assert result["fila_switch"] is not None
- assert result["fila_switch"]["installed"] is True
- assert result["fila_switch"]["in_slots"] == [-1, 2]
- assert result["fila_switch"]["out_extruders"] == [0, 1]
- assert result["fila_switch"]["stat"] == 0
- assert result["fila_switch"]["info"] == 2
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_cover_uses_dispatched_plate_when_gcode_file_lacks_path(
- self, async_client: AsyncClient, printer_factory, db_session, tmp_path
- ):
- """When firmware drops the plate path from gcode_file (e.g. P1S
- 01.10.00.00, #1166), the dispatched-plate record must take precedence
- and serve plate 4's thumbnail instead of falling back to plate_1.png."""
- import io
- import zipfile
- from unittest.mock import MagicMock, patch
- from backend.app.services.bambu_ftp import cache_3mf_download
- from backend.app.services.bambu_mqtt import PrinterState
- printer = await printer_factory()
- # Build a 3MF that mimics a "true" multi-plate archive: thumbnails
- # for plates 1..4 are all present, gcode files for plates 1..4 are
- # all present. Without the dispatch record we'd default to plate_1.png.
- threemf_path = tmp_path / "MyModel.3mf"
- with zipfile.ZipFile(threemf_path, "w") as zf:
- for plate in range(1, 5):
- zf.writestr(f"Metadata/plate_{plate}.png", f"PLATE_{plate}_PNG".encode())
- zf.writestr(f"Metadata/plate_{plate}.gcode", f"; plate {plate} gcode\n")
- cache_3mf_download(printer.id, "MyModel.3mf", threemf_path)
- state = PrinterState()
- state.connected = True
- state.state = "RUNNING"
- state.subtask_name = "MyModel"
- state.gcode_file = "MyModel.3mf" # firmware drops plate path
- state.dispatched_plate_id = 4
- state.dispatched_subtask = "MyModel"
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_status = MagicMock(return_value=state)
- mock_pm.is_awaiting_plate_clear = MagicMock(return_value=False)
- response = await async_client.get(f"/api/v1/printers/{printer.id}/cover")
- assert response.status_code == 200
- assert response.content == b"PLATE_4_PNG"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_cover_3mf_scan_fallback_for_per_plate_archive(
- self, async_client: AsyncClient, printer_factory, db_session, tmp_path
- ):
- """Per-plate archives sliced separately in Bambu Studio contain a
- single Metadata/plate_N.gcode (the active plate) but bundle thumbnails
- for every plate. With no dispatch record (e.g. dispatched via Studio
- directly) and no plate path in gcode_file, the route must scan the
- 3MF and pick plate N's thumbnail. See #1166 option 4."""
- import zipfile
- from unittest.mock import MagicMock, patch
- from backend.app.services.bambu_ftp import cache_3mf_download
- from backend.app.services.bambu_mqtt import PrinterState
- printer = await printer_factory()
- # Per-plate archive: thumbnails for all plates, gcode for plate 3 only.
- threemf_path = tmp_path / "PerPlate.3mf"
- with zipfile.ZipFile(threemf_path, "w") as zf:
- for plate in range(1, 5):
- zf.writestr(f"Metadata/plate_{plate}.png", f"PLATE_{plate}_PNG".encode())
- zf.writestr("Metadata/plate_3.gcode", "; only plate 3 has gcode\n")
- cache_3mf_download(printer.id, "PerPlate.3mf", threemf_path)
- state = PrinterState()
- state.connected = True
- state.state = "RUNNING"
- state.subtask_name = "PerPlate"
- state.gcode_file = "PerPlate.3mf"
- # No dispatch record (Studio-direct dispatch).
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_status = MagicMock(return_value=state)
- mock_pm.is_awaiting_plate_clear = MagicMock(return_value=False)
- response = await async_client.get(f"/api/v1/printers/{printer.id}/cover")
- assert response.status_code == 200
- assert response.content == b"PLATE_3_PNG"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_printer_status_omits_fila_switch_when_not_installed(
- self, async_client: AsyncClient, printer_factory, db_session
- ):
- """Without the FTS accessory, fila_switch must be null so the frontend
- keeps applying the per-extruder filter on regular dual-nozzle printers."""
- from unittest.mock import MagicMock, patch
- from backend.app.services.bambu_mqtt import PrinterState
- printer = await printer_factory()
- state = PrinterState()
- state.connected = True
- state.state = "IDLE"
- # default fila_switch — installed = False
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_status = MagicMock(return_value=state)
- mock_pm.is_awaiting_plate_clear = MagicMock(return_value=False)
- response = await async_client.get(f"/api/v1/printers/{printer.id}/status")
- assert response.status_code == 200
- result = response.json()
- assert result["fila_switch"] is None
- # ========================================================================
- # Test connection endpoint
- # ========================================================================
- class TestPrinterDataIntegrity:
- """Tests for printer data integrity."""
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_printer_stores_all_fields(self, async_client: AsyncClient, printer_factory, db_session):
- """Verify printer stores all fields correctly."""
- printer = await printer_factory(
- name="Full Test Printer",
- serial_number="00M09A444444444",
- ip_address="192.168.1.150",
- model="P1S",
- is_active=True,
- auto_archive=False,
- )
- response = await async_client.get(f"/api/v1/printers/{printer.id}")
- assert response.status_code == 200
- result = response.json()
- assert result["name"] == "Full Test Printer"
- assert result["serial_number"] == "00M09A444444444"
- assert result["ip_address"] == "192.168.1.150"
- assert result["model"] == "P1S"
- assert result["is_active"] is True
- assert result["auto_archive"] is False
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_printer_update_persists(self, async_client: AsyncClient, printer_factory, db_session):
- """CRITICAL: Verify printer updates persist."""
- printer = await printer_factory(name="Original", is_active=True)
- # Update
- await async_client.patch(f"/api/v1/printers/{printer.id}", json={"name": "Updated", "is_active": False})
- # Verify persistence
- response = await async_client.get(f"/api/v1/printers/{printer.id}")
- result = response.json()
- assert result["name"] == "Updated"
- assert result["is_active"] is False
- # ========================================================================
- # Refresh status endpoint
- # ========================================================================
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_refresh_status_not_found(self, async_client: AsyncClient):
- """Verify 404 for non-existent printer."""
- response = await async_client.post("/api/v1/printers/99999/refresh-status")
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_refresh_status_not_connected(self, async_client: AsyncClient, printer_factory):
- """Verify 400 when printer is not connected."""
- printer = await printer_factory(name="Disconnected Printer")
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.request_status_update.return_value = False
- response = await async_client.post(f"/api/v1/printers/{printer.id}/refresh-status")
- assert response.status_code == 400
- assert "not connected" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_refresh_status_success(self, async_client: AsyncClient, printer_factory):
- """Verify successful refresh request."""
- printer = await printer_factory(name="Connected Printer")
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.request_status_update.return_value = True
- response = await async_client.post(f"/api/v1/printers/{printer.id}/refresh-status")
- assert response.status_code == 200
- assert response.json()["status"] == "refresh_requested"
- mock_pm.request_status_update.assert_called_once_with(printer.id)
- # ========================================================================
- # Current print user endpoint (Issue #206)
- # ========================================================================
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_current_print_user_not_found(self, async_client: AsyncClient):
- """Verify 404 for non-existent printer."""
- response = await async_client.get("/api/v1/printers/99999/current-print-user")
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_current_print_user_returns_empty_when_no_user(self, async_client: AsyncClient, printer_factory):
- """Verify empty object returned when no user is tracked."""
- printer = await printer_factory(name="Test Printer")
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_current_print_user.return_value = None
- response = await async_client.get(f"/api/v1/printers/{printer.id}/current-print-user")
- assert response.status_code == 200
- assert response.json() == {}
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_current_print_user_returns_user_info(self, async_client: AsyncClient, printer_factory):
- """Verify user info is returned when tracked."""
- printer = await printer_factory(name="Test Printer")
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_current_print_user.return_value = {"user_id": 42, "username": "testuser"}
- response = await async_client.get(f"/api/v1/printers/{printer.id}/current-print-user")
- assert response.status_code == 200
- result = response.json()
- assert result["user_id"] == 42
- assert result["username"] == "testuser"
- class TestPrintControlAPI:
- """Integration tests for print control endpoints (stop, pause, resume)."""
- # ========================================================================
- # Stop print endpoint
- # ========================================================================
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_stop_print_not_found(self, async_client: AsyncClient):
- """Verify 404 for non-existent printer."""
- response = await async_client.post("/api/v1/printers/99999/print/stop")
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_stop_print_not_connected(self, async_client: AsyncClient, printer_factory):
- """Verify error when printer is not connected."""
- printer = await printer_factory(name="Disconnected Printer")
- 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}/print/stop")
- assert response.status_code == 400
- assert "not connected" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_stop_print_success(self, async_client: AsyncClient, printer_factory):
- """Verify successful stop print request."""
- printer = await printer_factory(name="Printing Printer")
- mock_client = MagicMock()
- mock_client.stop_print.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}/print/stop")
- assert response.status_code == 200
- assert response.json()["success"] is True
- mock_client.stop_print.assert_called_once()
- # ========================================================================
- # Pause print endpoint
- # ========================================================================
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_pause_print_not_found(self, async_client: AsyncClient):
- """Verify 404 for non-existent printer."""
- response = await async_client.post("/api/v1/printers/99999/print/pause")
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_pause_print_not_connected(self, async_client: AsyncClient, printer_factory):
- """Verify error when printer is not connected."""
- printer = await printer_factory(name="Disconnected Printer")
- 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}/print/pause")
- assert response.status_code == 400
- assert "not connected" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_pause_print_success(self, async_client: AsyncClient, printer_factory):
- """Verify successful pause print request."""
- printer = await printer_factory(name="Printing Printer")
- mock_client = MagicMock()
- mock_client.pause_print.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}/print/pause")
- assert response.status_code == 200
- assert response.json()["success"] is True
- mock_client.pause_print.assert_called_once()
- # ========================================================================
- # Resume print endpoint
- # ========================================================================
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_resume_print_not_found(self, async_client: AsyncClient):
- """Verify 404 for non-existent printer."""
- response = await async_client.post("/api/v1/printers/99999/print/resume")
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_resume_print_not_connected(self, async_client: AsyncClient, printer_factory):
- """Verify error when printer is not connected."""
- printer = await printer_factory(name="Disconnected Printer")
- 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}/print/resume")
- assert response.status_code == 400
- assert "not connected" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_resume_print_success(self, async_client: AsyncClient, printer_factory):
- """Verify successful resume print request."""
- printer = await printer_factory(name="Paused Printer")
- mock_client = MagicMock()
- mock_client.resume_print.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}/print/resume")
- assert response.status_code == 200
- assert response.json()["success"] is True
- mock_client.resume_print.assert_called_once()
- class TestAMSRefreshAPI:
- """Integration tests for AMS slot refresh endpoint."""
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_ams_refresh_not_found(self, async_client: AsyncClient):
- """Verify 404 for non-existent printer."""
- response = await async_client.post("/api/v1/printers/99999/ams/0/slot/0/refresh")
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_ams_refresh_not_connected(self, async_client: AsyncClient, printer_factory):
- """Verify error when printer is not connected."""
- printer = await printer_factory(name="Disconnected Printer")
- 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}/ams/0/slot/0/refresh")
- assert response.status_code == 400
- assert "not connected" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_ams_refresh_success(self, async_client: AsyncClient, printer_factory):
- """Verify successful AMS refresh request."""
- printer = await printer_factory(name="Printer with AMS")
- mock_client = MagicMock()
- mock_client.ams_refresh_tray.return_value = (True, "Refreshing AMS 0 tray 1")
- 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}/ams/0/slot/1/refresh")
- assert response.status_code == 200
- result = response.json()
- assert result["success"] is True
- mock_client.ams_refresh_tray.assert_called_once_with(0, 1)
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_ams_refresh_filament_loaded(self, async_client: AsyncClient, printer_factory):
- """Verify error when filament is loaded (can't refresh while loaded)."""
- printer = await printer_factory(name="Printer with AMS")
- mock_client = MagicMock()
- mock_client.ams_refresh_tray.return_value = (False, "Please unload filament first")
- 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}/ams/0/slot/0/refresh")
- assert response.status_code == 400
- assert "unload" in response.json()["detail"].lower()
- class TestAMSLoadUnloadAPI:
- """Integration tests for AMS load / unload endpoints (#891)."""
- # ── load ─────────────────────────────────────────────────────────────────
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_load_invalid_tray_id(self, async_client: AsyncClient, printer_factory):
- """tray_id outside {0..15, 254, 255} is rejected."""
- printer = await printer_factory(name="P")
- response = await async_client.post(f"/api/v1/printers/{printer.id}/ams/load?tray_id=99")
- assert response.status_code == 400
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_load_not_found(self, async_client: AsyncClient):
- response = await async_client.post("/api/v1/printers/99999/ams/load?tray_id=0")
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_load_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}/ams/load?tray_id=0")
- assert response.status_code == 400
- assert "not connected" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_load_ams_slot_success(self, async_client: AsyncClient, printer_factory):
- """tray_id=5 → AMS 1 slot 2 (1-indexed in the message)."""
- printer = await printer_factory(name="P")
- mock_client = MagicMock()
- mock_client.ams_load_filament.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}/ams/load?tray_id=5")
- assert response.status_code == 200
- mock_client.ams_load_filament.assert_called_once_with(5)
- assert "AMS 1" in response.json()["message"]
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_load_external_left_success(self, async_client: AsyncClient, printer_factory):
- """tray_id=254 → external spool / Ext-L."""
- printer = await printer_factory(name="P")
- mock_client = MagicMock()
- mock_client.ams_load_filament.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}/ams/load?tray_id=254")
- assert response.status_code == 200
- mock_client.ams_load_filament.assert_called_once_with(254)
- assert "external" in response.json()["message"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_load_external_right_success(self, async_client: AsyncClient, printer_factory):
- """tray_id=255 → Ext-R on dual-nozzle H2D."""
- printer = await printer_factory(name="H2D")
- mock_client = MagicMock()
- mock_client.ams_load_filament.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}/ams/load?tray_id=255")
- assert response.status_code == 200
- mock_client.ams_load_filament.assert_called_once_with(255)
- assert "Ext-R" in response.json()["message"]
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_load_mqtt_failure_returns_500(self, async_client: AsyncClient, printer_factory):
- printer = await printer_factory(name="P")
- mock_client = MagicMock()
- mock_client.ams_load_filament.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}/ams/load?tray_id=0")
- assert response.status_code == 500
- assert "failed" in response.json()["detail"].lower()
- # ── unload ───────────────────────────────────────────────────────────────
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_unload_not_found(self, async_client: AsyncClient):
- response = await async_client.post("/api/v1/printers/99999/ams/unload")
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_unload_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}/ams/unload")
- assert response.status_code == 400
- assert "not connected" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_unload_success(self, async_client: AsyncClient, printer_factory):
- printer = await printer_factory(name="P")
- mock_client = MagicMock()
- mock_client.ams_unload_filament.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}/ams/unload")
- assert response.status_code == 200
- mock_client.ams_unload_filament.assert_called_once_with()
- assert response.json()["success"] is True
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_unload_mqtt_failure_returns_500(self, async_client: AsyncClient, printer_factory):
- printer = await printer_factory(name="P")
- mock_client = MagicMock()
- mock_client.ams_unload_filament.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}/ams/unload")
- assert response.status_code == 500
- assert "failed" in response.json()["detail"].lower()
- class TestConfigureAMSSlotAPI:
- """Integration tests for AMS slot configure endpoint — tray_info_idx resolution."""
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_configure_not_connected(self, async_client: AsyncClient, printer_factory):
- """Verify error when printer is not connected."""
- 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}/slots/0/0/configure",
- params={
- "tray_info_idx": "GFL99",
- "tray_type": "PLA",
- "tray_sub_brands": "PLA Basic",
- "tray_color": "FF0000FF",
- "nozzle_temp_min": 190,
- "nozzle_temp_max": 230,
- },
- )
- assert response.status_code == 400
- assert "not connected" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_configure_with_gf_id_keeps_it(self, async_client: AsyncClient, printer_factory):
- """Standard Bambu GF* filament IDs are sent as-is."""
- printer = await printer_factory(name="H2D")
- mock_client = MagicMock()
- mock_client.ams_set_filament_setting.return_value = True
- mock_client.extrusion_cali_sel.return_value = True
- mock_client.request_status_update.return_value = True
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_client.return_value = mock_client
- mock_pm.get_status.return_value = None # No existing state
- response = await async_client.post(
- f"/api/v1/printers/{printer.id}/slots/2/3/configure",
- params={
- "tray_info_idx": "GFL05",
- "tray_type": "PLA",
- "tray_sub_brands": "PLA Basic",
- "tray_color": "FFFFFFFF",
- "nozzle_temp_min": 190,
- "nozzle_temp_max": 230,
- },
- )
- assert response.status_code == 200
- call_kwargs = mock_client.ams_set_filament_setting.call_args
- assert call_kwargs.kwargs["tray_info_idx"] == "GFL05"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_configure_pfus_sent_directly(self, async_client: AsyncClient, printer_factory):
- """PFUS* cloud-synced custom preset IDs are sent to the printer."""
- printer = await printer_factory(name="H2D")
- mock_client = MagicMock()
- mock_client.ams_set_filament_setting.return_value = True
- mock_client.extrusion_cali_sel.return_value = True
- mock_client.request_status_update.return_value = True
- mock_status = MagicMock()
- mock_status.raw_data = {"ams": {"ams": []}} # No existing tray data
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_client.return_value = mock_client
- mock_pm.get_status.return_value = mock_status
- response = await async_client.post(
- f"/api/v1/printers/{printer.id}/slots/2/3/configure",
- params={
- "tray_info_idx": "PFUS9ac902733670a9",
- "tray_type": "PLA",
- "tray_sub_brands": "Devil Design PLA",
- "tray_color": "FF0000FF",
- "nozzle_temp_min": 190,
- "nozzle_temp_max": 230,
- },
- )
- assert response.status_code == 200
- call_kwargs = mock_client.ams_set_filament_setting.call_args
- assert call_kwargs.kwargs["tray_info_idx"] == "PFUS9ac902733670a9"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_configure_pfus_takes_priority_over_slot(self, async_client: AsyncClient, printer_factory):
- """Provided PFUS* preset takes priority over slot's existing preset."""
- printer = await printer_factory(name="H2D")
- mock_client = MagicMock()
- mock_client.ams_set_filament_setting.return_value = True
- mock_client.extrusion_cali_sel.return_value = True
- mock_client.request_status_update.return_value = True
- # Simulate slot already configured by slicer with cloud-synced preset
- mock_status = MagicMock()
- mock_status.raw_data = {
- "ams": {
- "ams": [
- {
- "id": 2,
- "tray": [
- {
- "id": 3,
- "tray_info_idx": "P4d64437",
- "tray_type": "PLA",
- "tray_color": "FF0000FF",
- }
- ],
- }
- ]
- }
- }
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_client.return_value = mock_client
- mock_pm.get_status.return_value = mock_status
- response = await async_client.post(
- f"/api/v1/printers/{printer.id}/slots/2/3/configure",
- params={
- "tray_info_idx": "PFUS9ac902733670a9",
- "tray_type": "PLA",
- "tray_sub_brands": "Devil Design PLA",
- "tray_color": "FF0000FF",
- "nozzle_temp_min": 190,
- "nozzle_temp_max": 230,
- },
- )
- assert response.status_code == 200
- call_kwargs = mock_client.ams_set_filament_setting.call_args
- # Provided preset wins over slot's existing one
- assert call_kwargs.kwargs["tray_info_idx"] == "PFUS9ac902733670a9"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_configure_pfus_used_regardless_of_slot_material(self, async_client: AsyncClient, printer_factory):
- """Provided PFUS* preset is used even when slot has a different material."""
- printer = await printer_factory(name="H2D")
- mock_client = MagicMock()
- mock_client.ams_set_filament_setting.return_value = True
- mock_client.extrusion_cali_sel.return_value = True
- mock_client.request_status_update.return_value = True
- # Slot currently has PETG but user is configuring PLA
- mock_status = MagicMock()
- mock_status.raw_data = {
- "ams": {
- "ams": [
- {
- "id": 2,
- "tray": [{"id": 3, "tray_info_idx": "GFG99", "tray_type": "PETG", "tray_color": "FFFFFFFF"}],
- }
- ]
- }
- }
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_client.return_value = mock_client
- mock_pm.get_status.return_value = mock_status
- response = await async_client.post(
- f"/api/v1/printers/{printer.id}/slots/2/3/configure",
- params={
- "tray_info_idx": "PFUS9ac902733670a9",
- "tray_type": "PLA",
- "tray_sub_brands": "Devil Design PLA",
- "tray_color": "FF0000FF",
- "nozzle_temp_min": 190,
- "nozzle_temp_max": 230,
- },
- )
- assert response.status_code == 200
- call_kwargs = mock_client.ams_set_filament_setting.call_args
- # Provided preset wins — slot's material is irrelevant
- assert call_kwargs.kwargs["tray_info_idx"] == "PFUS9ac902733670a9"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_configure_empty_id_uses_generic(self, async_client: AsyncClient, printer_factory):
- """Empty tray_info_idx (local preset) is replaced with generic."""
- printer = await printer_factory(name="H2D")
- mock_client = MagicMock()
- mock_client.ams_set_filament_setting.return_value = True
- mock_client.extrusion_cali_sel.return_value = True
- mock_client.request_status_update.return_value = True
- mock_status = MagicMock()
- mock_status.raw_data = {"ams": {"ams": []}}
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_client.return_value = mock_client
- mock_pm.get_status.return_value = mock_status
- response = await async_client.post(
- f"/api/v1/printers/{printer.id}/slots/2/3/configure",
- params={
- "tray_info_idx": "",
- "tray_type": "PETG",
- "tray_sub_brands": "PETG Basic",
- "tray_color": "FFFFFFFF",
- "nozzle_temp_min": 220,
- "nozzle_temp_max": 260,
- },
- )
- assert response.status_code == 200
- call_kwargs = mock_client.ams_set_filament_setting.call_args
- assert call_kwargs.kwargs["tray_info_idx"] == "GFG99"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_configure_pfus_preserves_setting_id_pair(self, async_client: AsyncClient, printer_factory):
- """Both tray_info_idx=PFUS* and setting_id=PFUS* are forwarded untouched.
- Pins the end-to-end contract the frontend #1053 fix relies on: when the
- user configures a slot with a custom cloud preset whose cloud detail
- has filament_id=null, the frontend sends the setting_id in BOTH fields
- and the backend must not collapse either to a generic GF* ID.
- """
- printer = await printer_factory(name="H2D")
- mock_client = MagicMock()
- mock_client.ams_set_filament_setting.return_value = True
- mock_client.extrusion_cali_sel.return_value = True
- mock_client.request_status_update.return_value = True
- mock_status = MagicMock()
- mock_status.raw_data = {"ams": {"ams": []}}
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_client.return_value = mock_client
- mock_pm.get_status.return_value = mock_status
- response = await async_client.post(
- f"/api/v1/printers/{printer.id}/slots/128/0/configure",
- params={
- "tray_info_idx": "PFUSa8fb76f9733e3c",
- "tray_type": "ABS",
- "tray_sub_brands": "Sting3D ABS",
- "tray_color": "000000FF",
- "nozzle_temp_min": 240,
- "nozzle_temp_max": 280,
- "setting_id": "PFUSa8fb76f9733e3c",
- },
- )
- assert response.status_code == 200
- call_kwargs = mock_client.ams_set_filament_setting.call_args
- assert call_kwargs.kwargs["tray_info_idx"] == "PFUSa8fb76f9733e3c"
- assert call_kwargs.kwargs["setting_id"] == "PFUSa8fb76f9733e3c"
- # Explicitly assert no generic-collapse happened for this HT slot.
- assert call_kwargs.kwargs["tray_info_idx"] != "GFB99"
- class TestSkipObjectsAPI:
- """Integration tests for skip objects endpoints."""
- # ========================================================================
- # Get printable objects endpoint
- # ========================================================================
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_objects_not_found(self, async_client: AsyncClient):
- """Verify 404 for non-existent printer."""
- response = await async_client.get("/api/v1/printers/99999/print/objects")
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_objects_not_connected(self, async_client: AsyncClient, printer_factory):
- """Verify error when printer is not connected."""
- printer = await printer_factory(name="Disconnected Printer")
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_client.return_value = None
- response = await async_client.get(f"/api/v1/printers/{printer.id}/print/objects")
- assert response.status_code == 400
- assert "not connected" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_objects_empty(self, async_client: AsyncClient, printer_factory):
- """Verify empty objects list when no print is active."""
- printer = await printer_factory(name="Idle Printer")
- mock_client = MagicMock()
- mock_client.state.printable_objects = {}
- mock_client.state.skipped_objects = []
- mock_client.state.state = "IDLE"
- mock_client.state.subtask_name = None # Prevent FTP download attempt
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_client.return_value = mock_client
- response = await async_client.get(f"/api/v1/printers/{printer.id}/print/objects")
- assert response.status_code == 200
- result = response.json()
- assert result["objects"] == []
- assert result["total"] == 0
- assert result["skipped_count"] == 0
- assert result["is_printing"] is False
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_objects_with_data(self, async_client: AsyncClient, printer_factory):
- """Verify objects list when print is active."""
- printer = await printer_factory(name="Printing Printer")
- mock_client = MagicMock()
- mock_client.state.printable_objects = {100: "Part A", 200: "Part B", 300: "Part C"}
- mock_client.state.skipped_objects = [200]
- mock_client.state.state = "RUNNING"
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_client.return_value = mock_client
- response = await async_client.get(f"/api/v1/printers/{printer.id}/print/objects")
- assert response.status_code == 200
- result = response.json()
- assert result["total"] == 3
- assert result["skipped_count"] == 1
- assert result["is_printing"] is True
- # Check objects have correct structure
- objects_by_id = {obj["id"]: obj for obj in result["objects"]}
- assert objects_by_id[100]["name"] == "Part A"
- assert objects_by_id[100]["skipped"] is False
- assert objects_by_id[200]["name"] == "Part B"
- assert objects_by_id[200]["skipped"] is True
- assert objects_by_id[300]["name"] == "Part C"
- assert objects_by_id[300]["skipped"] is False
- # ========================================================================
- # Skip objects endpoint
- # ========================================================================
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_objects_with_positions(self, async_client: AsyncClient, printer_factory):
- """Verify objects list includes position data when available."""
- printer = await printer_factory(name="Printing Printer")
- # New format with position data
- mock_client = MagicMock()
- mock_client.state.printable_objects = {
- 100: {"name": "Part A", "x": 50.0, "y": 100.0},
- 200: {"name": "Part B", "x": 150.0, "y": 100.0},
- }
- mock_client.state.skipped_objects = []
- mock_client.state.state = "RUNNING"
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_client.return_value = mock_client
- response = await async_client.get(f"/api/v1/printers/{printer.id}/print/objects")
- assert response.status_code == 200
- result = response.json()
- assert result["total"] == 2
- # Check objects have position data
- objects_by_id = {obj["id"]: obj for obj in result["objects"]}
- assert objects_by_id[100]["name"] == "Part A"
- assert objects_by_id[100]["x"] == 50.0
- assert objects_by_id[100]["y"] == 100.0
- assert objects_by_id[200]["name"] == "Part B"
- assert objects_by_id[200]["x"] == 150.0
- assert objects_by_id[200]["y"] == 100.0
- # ========================================================================
- # Skip objects endpoint
- # ========================================================================
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_skip_objects_not_found(self, async_client: AsyncClient):
- """Verify 404 for non-existent printer."""
- response = await async_client.post("/api/v1/printers/99999/print/skip-objects", json=[100])
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_skip_objects_not_connected(self, async_client: AsyncClient, printer_factory):
- """Verify error when printer is not connected."""
- printer = await printer_factory(name="Disconnected Printer")
- 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}/print/skip-objects", json=[100])
- assert response.status_code == 400
- assert "not connected" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_skip_objects_empty_list(self, async_client: AsyncClient, printer_factory):
- """Verify error when no object IDs provided."""
- printer = await printer_factory(name="Printing Printer")
- mock_client = MagicMock()
- mock_client.state.printable_objects = {100: "Part A"}
- mock_client.state.skipped_objects = []
- 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}/print/skip-objects", json=[])
- assert response.status_code == 400
- assert "no object" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_skip_objects_invalid_id(self, async_client: AsyncClient, printer_factory):
- """Verify error when object ID doesn't exist."""
- printer = await printer_factory(name="Printing Printer")
- mock_client = MagicMock()
- mock_client.state.printable_objects = {100: "Part A"}
- mock_client.state.skipped_objects = []
- 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}/print/skip-objects", json=[999])
- assert response.status_code == 400
- assert "invalid" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_skip_objects_success(self, async_client: AsyncClient, printer_factory):
- """Verify successful skip objects request."""
- printer = await printer_factory(name="Printing Printer")
- mock_client = MagicMock()
- mock_client.state.printable_objects = {100: "Part A", 200: "Part B"}
- mock_client.state.skipped_objects = []
- mock_client.skip_objects.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}/print/skip-objects", json=[100])
- assert response.status_code == 200
- result = response.json()
- assert result["success"] is True
- assert 100 in result["skipped_objects"]
- mock_client.skip_objects.assert_called_once_with([100])
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_skip_objects_multiple(self, async_client: AsyncClient, printer_factory):
- """Verify skipping multiple objects at once."""
- printer = await printer_factory(name="Printing Printer")
- mock_client = MagicMock()
- mock_client.state.printable_objects = {100: "Part A", 200: "Part B", 300: "Part C"}
- mock_client.state.skipped_objects = []
- mock_client.skip_objects.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}/print/skip-objects", json=[100, 200])
- assert response.status_code == 200
- result = response.json()
- assert result["success"] is True
- assert 100 in result["skipped_objects"]
- assert 200 in result["skipped_objects"]
- mock_client.skip_objects.assert_called_once_with([100, 200])
- class TestChamberLightAPI:
- """Integration tests for chamber light control endpoint."""
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_chamber_light_not_found(self, async_client: AsyncClient):
- """Verify 404 for non-existent printer."""
- response = await async_client.post("/api/v1/printers/99999/chamber-light?on=true")
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_chamber_light_not_connected(self, async_client: AsyncClient, printer_factory):
- """Verify error when printer is not connected."""
- printer = await printer_factory(name="Disconnected Printer")
- 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}/chamber-light?on=true")
- assert response.status_code == 400
- assert "not connected" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_chamber_light_on_success(self, async_client: AsyncClient, printer_factory):
- """Verify successful chamber light on request."""
- printer = await printer_factory(name="Test Printer")
- mock_client = MagicMock()
- mock_client.set_chamber_light.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}/chamber-light?on=true")
- assert response.status_code == 200
- result = response.json()
- assert result["success"] is True
- assert "on" in result["message"].lower()
- mock_client.set_chamber_light.assert_called_once_with(True)
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_chamber_light_off_success(self, async_client: AsyncClient, printer_factory):
- """Verify successful chamber light off request."""
- printer = await printer_factory(name="Test Printer")
- mock_client = MagicMock()
- mock_client.set_chamber_light.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}/chamber-light?on=false")
- assert response.status_code == 200
- result = response.json()
- assert result["success"] is True
- assert "off" in result["message"].lower()
- mock_client.set_chamber_light.assert_called_once_with(False)
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_chamber_light_failure(self, async_client: AsyncClient, printer_factory):
- """Verify error handling when chamber light control fails."""
- printer = await printer_factory(name="Test Printer")
- mock_client = MagicMock()
- mock_client.set_chamber_light.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}/chamber-light?on=true")
- assert response.status_code == 500
- assert "failed" in response.json()["detail"].lower()
- class TestAirductModeAPI:
- """Integration tests for the airduct mode endpoint (P2S/H2*)."""
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_invalid_mode_rejected(self, async_client: AsyncClient, printer_factory):
- printer = await printer_factory(name="P", model="P2S")
- response = await async_client.post(f"/api/v1/printers/{printer.id}/airduct-mode?mode=foo")
- assert response.status_code == 400
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_not_connected(self, async_client: AsyncClient, printer_factory):
- printer = await printer_factory(name="P", model="P2S")
- 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}/airduct-mode?mode=cooling")
- assert response.status_code == 400
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_cooling_success(self, async_client: AsyncClient, printer_factory):
- printer = await printer_factory(name="P", model="P2S")
- mock_client = MagicMock()
- mock_client.set_airduct_mode.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}/airduct-mode?mode=cooling")
- assert response.status_code == 200
- assert response.json()["success"] is True
- mock_client.set_airduct_mode.assert_called_once_with("cooling")
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_heating_failure_returns_500(self, async_client: AsyncClient, printer_factory):
- printer = await printer_factory(name="P", model="P2S")
- mock_client = MagicMock()
- mock_client.set_airduct_mode.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}/airduct-mode?mode=heating")
- assert response.status_code == 500
- class TestClearHMSErrorsAPI:
- """Integration tests for clear HMS errors endpoint."""
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_clear_hms_errors_not_found(self, async_client: AsyncClient):
- """Verify 404 for non-existent printer."""
- response = await async_client.post("/api/v1/printers/99999/hms/clear")
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_clear_hms_errors_not_connected(self, async_client: AsyncClient, printer_factory):
- """Verify error when printer is not connected."""
- printer = await printer_factory(name="Disconnected Printer")
- 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}/hms/clear")
- assert response.status_code == 400
- assert "not connected" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_clear_hms_errors_success(self, async_client: AsyncClient, printer_factory):
- """Verify successful clear HMS errors request."""
- printer = await printer_factory(name="Test Printer")
- mock_client = MagicMock()
- mock_client.clear_hms_errors.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}/hms/clear")
- assert response.status_code == 200
- result = response.json()
- assert result["success"] is True
- assert "cleared" in result["message"].lower()
- mock_client.clear_hms_errors.assert_called_once()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_clear_hms_errors_failure(self, async_client: AsyncClient, printer_factory):
- """Verify error handling when clear HMS errors fails."""
- printer = await printer_factory(name="Test Printer")
- mock_client = MagicMock()
- mock_client.clear_hms_errors.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}/hms/clear")
- assert response.status_code == 500
- assert "failed" in response.json()["detail"].lower()
|