| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- """Integration tests for AMS Labels API endpoints."""
- from unittest.mock import MagicMock, patch
- import pytest
- from httpx import AsyncClient
- from backend.app.models.ams_label import AmsLabel
- class TestAmsLabelsAPI:
- """Integration tests for /api/v1/printers/{printer_id}/ams-labels endpoints."""
- def _mock_printer_state(self, ams_units=None):
- """Create a mock printer state with AMS data."""
- state = MagicMock()
- state.connected = True
- state.raw_data = {
- "ams": ams_units
- or [
- {"id": "0", "sn": "AMS_SERIAL_0"},
- {"id": "1", "sn": "AMS_SERIAL_1"},
- ],
- }
- return state
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_labels_empty(self, async_client: AsyncClient, printer_factory):
- """Returns empty dict when no labels are saved."""
- printer = await printer_factory()
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_status.return_value = self._mock_printer_state()
- response = await async_client.get(f"/api/v1/printers/{printer.id}/ams-labels")
- assert response.status_code == 200
- assert response.json() == {}
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_save_label_with_serial(self, async_client: AsyncClient, printer_factory):
- """Save a label keyed by AMS serial number."""
- printer = await printer_factory()
- response = await async_client.put(
- f"/api/v1/printers/{printer.id}/ams-labels/0",
- json={"label": "Workshop AMS", "ams_serial": "AMS_SERIAL_0"},
- )
- assert response.status_code == 200
- assert response.json() == {"ams_id": 0, "label": "Workshop AMS"}
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_save_label_without_serial_uses_synthetic_key(
- self, async_client: AsyncClient, printer_factory, db_session
- ):
- """When no serial is provided, a synthetic key p{printer_id}a{ams_id} is used."""
- printer = await printer_factory()
- response = await async_client.put(
- f"/api/v1/printers/{printer.id}/ams-labels/2",
- json={"label": "Old Firmware AMS"},
- )
- assert response.status_code == 200
- # Verify the synthetic key was stored
- from sqlalchemy import select
- result = await db_session.execute(select(AmsLabel).where(AmsLabel.ams_serial_number == f"p{printer.id}a2"))
- label = result.scalar_one_or_none()
- assert label is not None
- assert label.label == "Old Firmware AMS"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_save_label_whitespace_serial_uses_synthetic_key(
- self, async_client: AsyncClient, printer_factory, db_session
- ):
- """Whitespace-only serial falls back to synthetic key."""
- printer = await printer_factory()
- response = await async_client.put(
- f"/api/v1/printers/{printer.id}/ams-labels/0",
- json={"label": "Whitespace Test", "ams_serial": " "},
- )
- assert response.status_code == 200
- from sqlalchemy import select
- result = await db_session.execute(select(AmsLabel).where(AmsLabel.ams_serial_number == f"p{printer.id}a0"))
- label = result.scalar_one_or_none()
- assert label is not None
- assert label.label == "Whitespace Test"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_save_label_updates_existing(self, async_client: AsyncClient, printer_factory):
- """Saving a label with the same serial updates the existing record."""
- printer = await printer_factory()
- await async_client.put(
- f"/api/v1/printers/{printer.id}/ams-labels/0",
- json={"label": "Original Name", "ams_serial": "SN123"},
- )
- response = await async_client.put(
- f"/api/v1/printers/{printer.id}/ams-labels/0",
- json={"label": "Updated Name", "ams_serial": "SN123"},
- )
- assert response.status_code == 200
- assert response.json()["label"] == "Updated Name"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_save_label_printer_not_found(self, async_client: AsyncClient):
- """Returns 404 when printer does not exist."""
- response = await async_client.put(
- "/api/v1/printers/99999/ams-labels/0",
- json={"label": "Ghost Printer"},
- )
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_save_label_validation_empty_label(self, async_client: AsyncClient, printer_factory):
- """Rejects empty label."""
- printer = await printer_factory()
- response = await async_client.put(
- f"/api/v1/printers/{printer.id}/ams-labels/0",
- json={"label": ""},
- )
- assert response.status_code == 422
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_labels_resolves_serial_to_ams_id(self, async_client: AsyncClient, printer_factory):
- """GET returns labels keyed by ams_id, resolved from live printer state."""
- printer = await printer_factory()
- # Save a label with a known serial
- await async_client.put(
- f"/api/v1/printers/{printer.id}/ams-labels/0",
- json={"label": "Silk Colours", "ams_serial": "AMS_SERIAL_0"},
- )
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_status.return_value = self._mock_printer_state()
- response = await async_client.get(f"/api/v1/printers/{printer.id}/ams-labels")
- assert response.status_code == 200
- data = response.json()
- assert data.get("0") == "Silk Colours"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_labels_no_printer_state(self, async_client: AsyncClient, printer_factory):
- """GET returns empty when printer has no live state."""
- printer = await printer_factory()
- with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
- mock_pm.get_status.return_value = None
- response = await async_client.get(f"/api/v1/printers/{printer.id}/ams-labels")
- assert response.status_code == 200
- assert response.json() == {}
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_delete_label(self, async_client: AsyncClient, printer_factory, db_session):
- """Delete removes the label from the database."""
- printer = await printer_factory()
- await async_client.put(
- f"/api/v1/printers/{printer.id}/ams-labels/0",
- json={"label": "To Delete", "ams_serial": "DEL_SN"},
- )
- response = await async_client.delete(f"/api/v1/printers/{printer.id}/ams-labels/0?ams_serial=DEL_SN")
- assert response.status_code == 200
- assert response.json() == {"success": True}
- # Verify it's gone
- from sqlalchemy import select
- result = await db_session.execute(select(AmsLabel).where(AmsLabel.ams_serial_number == "DEL_SN"))
- assert result.scalar_one_or_none() is None
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_delete_nonexistent_label_succeeds(self, async_client: AsyncClient, printer_factory):
- """Delete returns success even if no label exists (idempotent)."""
- printer = await printer_factory()
- response = await async_client.delete(f"/api/v1/printers/{printer.id}/ams-labels/0?ams_serial=NONEXISTENT")
- assert response.status_code == 200
- assert response.json() == {"success": True}
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_delete_label_whitespace_serial_uses_synthetic_key(
- self, async_client: AsyncClient, printer_factory, db_session
- ):
- """Delete with whitespace serial falls back to synthetic key."""
- printer = await printer_factory()
- # Save with synthetic key
- await async_client.put(
- f"/api/v1/printers/{printer.id}/ams-labels/0",
- json={"label": "Synthetic Label"},
- )
- response = await async_client.delete(f"/api/v1/printers/{printer.id}/ams-labels/0?ams_serial=%20%20")
- assert response.status_code == 200
- from sqlalchemy import select
- result = await db_session.execute(select(AmsLabel).where(AmsLabel.ams_serial_number == f"p{printer.id}a0"))
- assert result.scalar_one_or_none() is None
|