Files
bambuddy/backend/tests/integration/test_ams_labels_api.py
maziggy ed462c5cca Add tests, docs, and lint fix for AMS Info Card feature (#570)
- Add 12 backend integration tests for AMS labels API (GET/PUT/DELETE,
    serial resolution, synthetic key fallback, whitespace handling, validation)
  - Add 10 frontend tests for FilamentSlotCircle component (rendering,
    border styles, background colors, text contrast inversion)
  - Fix ruff W293 trailing whitespace in inventory.py from contributor fix
  - Add ams_label model import to test conftest.py
  - Update CHANGELOG, README, website features page, and wiki AMS docs
2026-03-05 08:46:39 +01:00

208 lines
8.4 KiB
Python

"""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