bambuddy/backend/tests/integration/test_maintenance_api.py
maziggy 673001e3cd fix(maintenance): #1596 persist wiki_url on Custom Type create + correct
inline mutation type

  POST /api/v1/maintenance/types hard-coded the MaintenanceType
  constructor and silently dropped `wiki_url`, so the Documentation URL
  field disappeared after save. PATCH worked because it uses
  `data.model_dump(exclude_unset=True) + setattr`, which is why editing
  a freshly-created type DID save the URL — masking the bug under any
  "save then immediately fix it" retest. Reporter @BurntOutHylian
  pre-triaged the issue to the exact constructor call at
  routes/maintenance.py:206-213; fix is the missing `wiki_url=data.wiki_url`
  argument.

  Frontend nit from the same report: MaintenancePage.tsx:1131's
  `updateTypeMutation` declared `data: Partial<{ name; default_interval_hours;
  interval_type; icon }>` — omitting `wiki_url`. The value reached the
  API correctly at runtime because `api.updateMaintenanceType` accepts
  `Partial<MaintenanceTypeCreate>` (which has wiki_url), but the inline
  type lied about the payload shape. Extended the inline `Partial<{...}>`
  to include `wiki_url?: string | null`. Pure type fix — no runtime change.
2026-06-02 14:58:10 +02:00

258 lines
11 KiB
Python

"""Integration tests for Maintenance API endpoints."""
import pytest
from httpx import AsyncClient
class TestMaintenanceTypesAPI:
"""Integration tests for /api/v1/maintenance/types endpoints."""
@pytest.mark.asyncio
@pytest.mark.integration
async def test_list_maintenance_types(self, async_client: AsyncClient):
"""Verify maintenance types list returns data with defaults."""
response = await async_client.get("/api/v1/maintenance/types")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
# Should have default system types
assert len(data) >= 1
@pytest.mark.asyncio
@pytest.mark.integration
async def test_list_includes_system_types(self, async_client: AsyncClient):
"""Verify default system types are created."""
response = await async_client.get("/api/v1/maintenance/types")
assert response.status_code == 200
data = response.json()
names = [t["name"] for t in data]
# Check for some default types
assert "Lubricate Linear Rails" in names or len(data) > 0
@pytest.mark.asyncio
@pytest.mark.integration
async def test_create_custom_maintenance_type(self, async_client: AsyncClient):
"""Verify custom maintenance type can be created."""
data = {
"name": "Custom Test Task",
"description": "Test description",
"default_interval_hours": 200.0,
"interval_type": "hours",
"icon": "Wrench",
}
response = await async_client.post("/api/v1/maintenance/types", json=data)
assert response.status_code == 200
result = response.json()
assert result["name"] == "Custom Test Task"
assert result["is_system"] is False
@pytest.mark.asyncio
@pytest.mark.integration
async def test_create_custom_type_persists_wiki_url(self, async_client: AsyncClient):
"""#1596: pre-fix, the POST handler hard-coded every constructor field
by name and silently dropped `wiki_url`. The schema accepted the value,
the response echoed `null`, and the row landed without it. Pin the
contract so the constructor doesn't drift again."""
data = {
"name": "Wiki URL Persistence Test",
"default_interval_hours": 50.0,
"interval_type": "hours",
"wiki_url": "https://wiki.example.com/lubrication",
}
response = await async_client.post("/api/v1/maintenance/types", json=data)
assert response.status_code == 200
assert response.json()["wiki_url"] == "https://wiki.example.com/lubrication"
# Verify it persists through a separate GET round-trip — the POST
# response could have echoed the request body without committing.
list_response = await async_client.get("/api/v1/maintenance/types")
assert list_response.status_code == 200
matching = [t for t in list_response.json() if t["name"] == data["name"]]
assert len(matching) == 1
assert matching[0]["wiki_url"] == "https://wiki.example.com/lubrication"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_update_maintenance_type(self, async_client: AsyncClient):
"""Verify maintenance type can be updated."""
# First create a custom type
create_data = {
"name": "Update Test",
"description": "Original",
"default_interval_hours": 100.0,
}
create_response = await async_client.post("/api/v1/maintenance/types", json=create_data)
assert create_response.status_code == 200
type_id = create_response.json()["id"]
# Update it
update_data = {"description": "Updated description"}
response = await async_client.patch(f"/api/v1/maintenance/types/{type_id}", json=update_data)
assert response.status_code == 200
assert response.json()["description"] == "Updated description"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_delete_custom_maintenance_type(self, async_client: AsyncClient):
"""Verify custom maintenance type can be deleted."""
# Create a custom type
create_data = {
"name": "Delete Test",
"description": "To be deleted",
"default_interval_hours": 50.0,
}
create_response = await async_client.post("/api/v1/maintenance/types", json=create_data)
type_id = create_response.json()["id"]
# Delete it
response = await async_client.delete(f"/api/v1/maintenance/types/{type_id}")
assert response.status_code == 200
class TestPrinterMaintenanceAPI:
"""Integration tests for /api/v1/maintenance/printers endpoints."""
@pytest.mark.asyncio
@pytest.mark.integration
async def test_get_printer_maintenance_not_found(self, async_client: AsyncClient):
"""Verify 404 for non-existent printer."""
response = await async_client.get("/api/v1/maintenance/printers/9999")
assert response.status_code == 404
@pytest.mark.asyncio
@pytest.mark.integration
async def test_get_printer_maintenance(self, async_client: AsyncClient, printer_factory, db_session):
"""Verify maintenance overview for a printer."""
printer = await printer_factory(name="Maintenance Test Printer")
response = await async_client.get(f"/api/v1/maintenance/printers/{printer.id}")
assert response.status_code == 200
data = response.json()
assert data["printer_id"] == printer.id
assert data["printer_name"] == "Maintenance Test Printer"
assert "maintenance_items" in data
assert "total_print_hours" in data
@pytest.mark.asyncio
@pytest.mark.integration
async def test_get_all_maintenance_overview(self, async_client: AsyncClient, printer_factory, db_session):
"""Verify overview endpoint returns all printers."""
await printer_factory(name="Overview Printer 1")
await printer_factory(name="Overview Printer 2")
response = await async_client.get("/api/v1/maintenance/overview")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
@pytest.mark.asyncio
@pytest.mark.integration
async def test_get_maintenance_summary(self, async_client: AsyncClient):
"""Verify summary endpoint returns counts."""
response = await async_client.get("/api/v1/maintenance/summary")
assert response.status_code == 200
data = response.json()
assert "total_due" in data
assert "total_warning" in data
assert "printers_with_issues" in data
class TestMaintenanceItemsAPI:
"""Integration tests for /api/v1/maintenance/items endpoints."""
@pytest.fixture
async def maintenance_item(self, async_client: AsyncClient, printer_factory, db_session):
"""Create a maintenance item for testing."""
printer = await printer_factory(name="Item Test Printer")
# Get the printer's maintenance overview to create items
response = await async_client.get(f"/api/v1/maintenance/printers/{printer.id}")
assert response.status_code == 200
data = response.json()
# Return the first maintenance item
if data["maintenance_items"]:
return data["maintenance_items"][0]
return None
@pytest.mark.asyncio
@pytest.mark.integration
async def test_update_maintenance_item(self, async_client: AsyncClient, maintenance_item):
"""Verify maintenance item can be updated."""
if not maintenance_item:
pytest.skip("No maintenance items available")
item_id = maintenance_item["id"]
response = await async_client.patch(
f"/api/v1/maintenance/items/{item_id}", json={"custom_interval_hours": 150.0}
)
assert response.status_code == 200
@pytest.mark.asyncio
@pytest.mark.integration
async def test_disable_maintenance_item(self, async_client: AsyncClient, maintenance_item):
"""Verify maintenance item can be disabled."""
if not maintenance_item:
pytest.skip("No maintenance items available")
item_id = maintenance_item["id"]
response = await async_client.patch(f"/api/v1/maintenance/items/{item_id}", json={"enabled": False})
assert response.status_code == 200
assert response.json()["enabled"] is False
@pytest.mark.asyncio
@pytest.mark.integration
async def test_perform_maintenance(self, async_client: AsyncClient, maintenance_item):
"""Verify maintenance can be marked as performed."""
if not maintenance_item:
pytest.skip("No maintenance items available")
item_id = maintenance_item["id"]
response = await async_client.post(
f"/api/v1/maintenance/items/{item_id}/perform", json={"notes": "Test maintenance performed"}
)
assert response.status_code == 200
data = response.json()
assert data["last_performed_at"] is not None
@pytest.mark.asyncio
@pytest.mark.integration
async def test_get_maintenance_history(self, async_client: AsyncClient, maintenance_item):
"""Verify maintenance history can be retrieved."""
if not maintenance_item:
pytest.skip("No maintenance items available")
item_id = maintenance_item["id"]
# First perform maintenance to create history
await async_client.post(f"/api/v1/maintenance/items/{item_id}/perform", json={"notes": "History test"})
response = await async_client.get(f"/api/v1/maintenance/items/{item_id}/history")
assert response.status_code == 200
history = response.json()
assert isinstance(history, list)
@pytest.mark.asyncio
@pytest.mark.integration
async def test_update_maintenance_item_not_found(self, async_client: AsyncClient):
"""Verify 404 for non-existent maintenance item."""
response = await async_client.patch("/api/v1/maintenance/items/9999", json={"enabled": False})
assert response.status_code == 404
class TestPrinterHoursAPI:
"""Integration tests for /api/v1/maintenance/printers/{id}/hours endpoint."""
@pytest.mark.asyncio
@pytest.mark.integration
async def test_set_printer_hours(self, async_client: AsyncClient, printer_factory, db_session):
"""Verify printer hours can be set."""
printer = await printer_factory(name="Hours Test Printer")
response = await async_client.patch(
f"/api/v1/maintenance/printers/{printer.id}/hours", params={"total_hours": 500.0}
)
assert response.status_code == 200
data = response.json()
assert data["total_hours"] == 500.0
@pytest.mark.asyncio
@pytest.mark.integration
async def test_set_printer_hours_not_found(self, async_client: AsyncClient):
"""Verify 404 for non-existent printer."""
response = await async_client.patch("/api/v1/maintenance/printers/9999/hours", params={"total_hours": 100.0})
assert response.status_code == 404