"""Zubehör: ein Einzelstück gehört zu einem anderen (Netzteil → Laptop). Ein Teil hängt an genau einem Gerät, ein Gerät trägt beliebig viele Teile. Geprüft werden die Zuordnung selbst, ihre Grenzen (Selbstbezug, Ring) und das Mitverleihen, das die Oberfläche erfragt und der Server nie von sich aus tut. """ import pytest from fastapi import HTTPException from app.models import Category, CategoryTracking, Item, Product, Role, User from app.routers.items import ( create_items, delete_item, get_item, zubehoer_loesen, zubehoer_zuordnen, ) from app.routers.loans import lend_item, return_item from app.schemas import ItemCreate, LoanCreate, LoanReturn, ZubehoerZuordnen from app.services.loans import offener_verleih @pytest.fixture() def admin(db): person = User(username="admin", password_hash="x", role=Role.admin) db.add(person) db.commit() db.refresh(person) return person @pytest.fixture() def laptop(db): cat = Category(name="Elektronik", tracking=CategoryTracking.object.value) db.add(cat) db.flush() product = Product(name="Laptop", category_id=cat.id, individual=True, package_size=1) db.add(product) db.commit() db.refresh(product) return product def _stueck(db, admin, product) -> Item: out = create_items(product.id, ItemCreate(count=1), db=db, user=admin)[0] return db.get(Item, out.id) def _haenge(db, admin, geraet: Item, teil: Item): return zubehoer_zuordnen( geraet.id, ZubehoerZuordnen(child_item_id=teil.id), db=db, _=admin ) # -------------------------------------------------------------------------- # Zuordnen und lösen # -------------------------------------------------------------------------- def test_geraet_traegt_mehrere_teile(db, admin, laptop): geraet = _stueck(db, admin, laptop) netzteil = _stueck(db, admin, laptop) tasche = _stueck(db, admin, laptop) _haenge(db, admin, geraet, netzteil) out = _haenge(db, admin, geraet, tasche) assert {t.uid for t in out.zubehoer} == {netzteil.uid, tasche.uid} # Und die Gegenrichtung steht am Teil. assert get_item(netzteil.id, db=db, _=admin).gehoert_zu.uid == geraet.uid def test_teil_wechselt_das_geraet_ohne_dublette(db, admin, laptop): """Ein Teil haengt an genau einem Geraet – Umhaengen ist ein Wechsel.""" a = _stueck(db, admin, laptop) b = _stueck(db, admin, laptop) netzteil = _stueck(db, admin, laptop) _haenge(db, admin, a, netzteil) _haenge(db, admin, b, netzteil) assert get_item(a.id, db=db, _=admin).zubehoer == [] assert [t.uid for t in get_item(b.id, db=db, _=admin).zubehoer] == [netzteil.uid] def test_loesen_laesst_das_stueck_bestehen(db, admin, laptop): geraet = _stueck(db, admin, laptop) netzteil = _stueck(db, admin, laptop) _haenge(db, admin, geraet, netzteil) zubehoer_loesen(geraet.id, netzteil.id, db=db, _=admin) assert get_item(geraet.id, db=db, _=admin).zubehoer == [] assert db.get(Item, netzteil.id) is not None def test_loesen_eines_fremden_teils_ist_ein_fehler(db, admin, laptop): geraet = _stueck(db, admin, laptop) fremdes = _stueck(db, admin, laptop) with pytest.raises(HTTPException) as fehler: zubehoer_loesen(geraet.id, fremdes.id, db=db, _=admin) assert fehler.value.status_code == 404 # -------------------------------------------------------------------------- # Grenzen # -------------------------------------------------------------------------- def test_selbstbezug_wird_abgelehnt(db, admin, laptop): geraet = _stueck(db, admin, laptop) with pytest.raises(HTTPException) as fehler: _haenge(db, admin, geraet, geraet) assert fehler.value.status_code == 409 def test_ring_wird_abgelehnt(db, admin, laptop): """A trägt B – dann darf B nicht A tragen, sonst läuft jede Kette endlos.""" a = _stueck(db, admin, laptop) b = _stueck(db, admin, laptop) _haenge(db, admin, a, b) with pytest.raises(HTTPException) as fehler: _haenge(db, admin, b, a) assert fehler.value.status_code == 409 def test_laengerer_ring_wird_abgelehnt(db, admin, laptop): a, b, c = (_stueck(db, admin, laptop) for _ in range(3)) _haenge(db, admin, a, b) _haenge(db, admin, b, c) with pytest.raises(HTTPException) as fehler: _haenge(db, admin, c, a) assert fehler.value.status_code == 409 def test_geraet_loeschen_gibt_das_teil_frei(db, admin, laptop): """Das Netzteil ueberlebt den Laptop – es wird wieder eigenstaendig.""" geraet = _stueck(db, admin, laptop) netzteil = _stueck(db, admin, laptop) _haenge(db, admin, geraet, netzteil) delete_item(geraet.id, db=db, _=admin) uebrig = db.get(Item, netzteil.id) assert uebrig is not None assert uebrig.parent_item_id is None # -------------------------------------------------------------------------- # Verleihen und Zurücknehmen mit Zubehör # -------------------------------------------------------------------------- def test_ohne_angabe_geht_nur_das_geraet(db, admin, laptop): """Der Server nimmt nie ungefragt etwas mit.""" geraet = _stueck(db, admin, laptop) netzteil = _stueck(db, admin, laptop) _haenge(db, admin, geraet, netzteil) lend_item(geraet.id, LoanCreate(borrower="Markus"), db=db, user=admin) assert offener_verleih(db, geraet.id) is not None assert offener_verleih(db, netzteil.id) is None def test_mit_zubehoer_verleihen_und_zuruecknehmen(db, admin, laptop): geraet = _stueck(db, admin, laptop) netzteil = _stueck(db, admin, laptop) tasche = _stueck(db, admin, laptop) _haenge(db, admin, geraet, netzteil) _haenge(db, admin, geraet, tasche) teile = [netzteil.id, tasche.id] lend_item( geraet.id, LoanCreate(borrower="Markus", mit_zubehoer=teile), db=db, user=admin, ) assert all(offener_verleih(db, i) is not None for i in [geraet.id, *teile]) return_item(geraet.id, LoanReturn(mit_zubehoer=teile), db=db, _=admin) # Keines darf offen haengenbleiben – sonst sucht es niemand mehr. assert all(offener_verleih(db, i) is None for i in [geraet.id, *teile]) def test_fremdes_stueck_laesst_sich_nicht_mitverleihen(db, admin, laptop): geraet = _stueck(db, admin, laptop) fremdes = _stueck(db, admin, laptop) with pytest.raises(HTTPException) as fehler: lend_item( geraet.id, LoanCreate(borrower="Markus", mit_zubehoer=[fremdes.id]), db=db, user=admin, ) assert fehler.value.status_code == 400 assert offener_verleih(db, fremdes.id) is None def test_zuruecknehmen_uebergeht_ein_teil_das_dablieb(db, admin, laptop): """Wer das Netzteil behalten hat, verleiht es nicht – die Rückgabe darf daran trotzdem nicht scheitern.""" geraet = _stueck(db, admin, laptop) netzteil = _stueck(db, admin, laptop) _haenge(db, admin, geraet, netzteil) lend_item(geraet.id, LoanCreate(borrower="Markus"), db=db, user=admin) return_item(geraet.id, LoanReturn(mit_zubehoer=[netzteil.id]), db=db, _=admin) assert offener_verleih(db, geraet.id) is None