| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212 |
- """Bulk inventory endpoint coverage for the batch-edit feature (#1795).
- Endpoints under test:
- - POST /api/v1/inventory/spools/bulk-update
- - POST /api/v1/inventory/spools/bulk-delete
- - POST /api/v1/inventory/spools/bulk-archive
- - POST /api/v1/inventory/spools/bulk-restore
- The Spoolman-mode equivalents live in test_spoolman_inventory_api.py.
- """
- from datetime import datetime, timezone
- import pytest
- from httpx import AsyncClient
- from sqlalchemy import select
- from sqlalchemy.ext.asyncio import AsyncSession
- from backend.app.models.spool import Spool
- @pytest.fixture
- async def spool_factory(db_session: AsyncSession):
- async def _create(**kwargs):
- defaults = {
- "material": "PLA",
- "subtype": "Basic",
- "brand": "Bambu",
- "color_name": "Red",
- "rgba": "FF0000FF",
- "label_weight": 1000,
- "core_weight": 250,
- "weight_used": 0,
- "weight_used_baseline": 0,
- "weight_locked": False,
- }
- defaults.update(kwargs)
- spool = Spool(**defaults)
- db_session.add(spool)
- await db_session.commit()
- await db_session.refresh(spool)
- return spool
- return _create
- class TestBulkUpdate:
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_applies_patch_to_all_listed_spools(self, async_client: AsyncClient, spool_factory, db_session):
- a = await spool_factory(brand="Bambu", note=None)
- b = await spool_factory(brand="Bambu", note=None)
- c = await spool_factory(brand="Bambu", note=None)
- resp = await async_client.post(
- "/api/v1/inventory/spools/bulk-update",
- json={"ids": [a.id, b.id, c.id], "update": {"brand": "Sunlu", "note": "From bulk edit"}},
- )
- assert resp.status_code == 200
- body = resp.json()
- assert body["updated"] == 3
- assert body["not_found"] == []
- for spool in (a, b, c):
- await db_session.refresh(spool)
- assert spool.brand == "Sunlu"
- assert spool.note == "From bulk edit"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_reports_unknown_ids_in_not_found(self, async_client: AsyncClient, spool_factory, db_session):
- real = await spool_factory(brand="Bambu")
- resp = await async_client.post(
- "/api/v1/inventory/spools/bulk-update",
- json={"ids": [real.id, 999_999], "update": {"brand": "Sunlu"}},
- )
- assert resp.status_code == 200
- body = resp.json()
- assert body["updated"] == 1
- assert body["not_found"] == [999_999]
- await db_session.refresh(real)
- assert real.brand == "Sunlu"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_empty_update_rejected(self, async_client: AsyncClient, spool_factory):
- a = await spool_factory()
- resp = await async_client.post(
- "/api/v1/inventory/spools/bulk-update",
- json={"ids": [a.id], "update": {}},
- )
- assert resp.status_code == 400
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_setting_weight_used_auto_locks(self, async_client: AsyncClient, spool_factory, db_session):
- a = await spool_factory(weight_locked=False, weight_used=0.0)
- resp = await async_client.post(
- "/api/v1/inventory/spools/bulk-update",
- json={"ids": [a.id], "update": {"weight_used": 250.5}},
- )
- assert resp.status_code == 200
- await db_session.refresh(a)
- assert a.weight_used == 250.5
- assert a.weight_locked is True
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_empty_ids_list_rejected(self, async_client: AsyncClient):
- resp = await async_client.post(
- "/api/v1/inventory/spools/bulk-update",
- json={"ids": [], "update": {"brand": "X"}},
- )
- assert resp.status_code == 422
- class TestBulkDelete:
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_deletes_listed_spools(self, async_client: AsyncClient, spool_factory, db_session):
- a = await spool_factory()
- b = await spool_factory()
- kept = await spool_factory()
- resp = await async_client.post(
- "/api/v1/inventory/spools/bulk-delete",
- json={"ids": [a.id, b.id]},
- )
- assert resp.status_code == 200
- body = resp.json()
- assert body["deleted"] == 2
- assert body["not_found"] == []
- remaining = (await db_session.execute(select(Spool.id))).scalars().all()
- assert kept.id in remaining
- assert a.id not in remaining
- assert b.id not in remaining
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_reports_unknown_ids(self, async_client: AsyncClient, spool_factory):
- a = await spool_factory()
- resp = await async_client.post(
- "/api/v1/inventory/spools/bulk-delete",
- json={"ids": [a.id, 999_999]},
- )
- assert resp.status_code == 200
- body = resp.json()
- assert body["deleted"] == 1
- assert body["not_found"] == [999_999]
- class TestBulkArchiveRestore:
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_bulk_archive_sets_archived_at(self, async_client: AsyncClient, spool_factory, db_session):
- a = await spool_factory()
- b = await spool_factory()
- resp = await async_client.post(
- "/api/v1/inventory/spools/bulk-archive",
- json={"ids": [a.id, b.id]},
- )
- assert resp.status_code == 200
- body = resp.json()
- assert body["archived"] == 2
- assert body["already_archived"] == []
- assert body["not_found"] == []
- for s in (a, b):
- await db_session.refresh(s)
- assert s.archived_at is not None
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_bulk_archive_skips_already_archived(self, async_client: AsyncClient, spool_factory, db_session):
- active = await spool_factory()
- already = await spool_factory(archived_at=datetime.now(timezone.utc))
- resp = await async_client.post(
- "/api/v1/inventory/spools/bulk-archive",
- json={"ids": [active.id, already.id]},
- )
- assert resp.status_code == 200
- body = resp.json()
- assert body["archived"] == 1
- assert body["already_archived"] == [already.id]
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_bulk_restore_clears_archived_at(self, async_client: AsyncClient, spool_factory, db_session):
- archived = await spool_factory(archived_at=datetime.now(timezone.utc))
- active = await spool_factory(archived_at=None)
- resp = await async_client.post(
- "/api/v1/inventory/spools/bulk-restore",
- json={"ids": [archived.id, active.id]},
- )
- assert resp.status_code == 200
- body = resp.json()
- assert body["restored"] == 1
- assert body["already_active"] == [active.id]
- await db_session.refresh(archived)
- assert archived.archived_at is None
|