mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
- 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
208 lines
8.4 KiB
Python
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
|