from pathlib import Path import app.db as db_module from app.models import Box, Item, SubItem def create_box(client, name="Box A", note="Packed", room="Bedroom", status="ready"): return client.post( "/boxes", data={"name": name, "note": note, "room": room, "status": status}, follow_redirects=False, ) def create_item(client, box_id, name="Item A", note="Note", quantity="2", is_container=False): data = {"name": name, "note": note, "quantity": quantity} if is_container: data["is_container"] = "on" return client.post(f"/boxes/{box_id}/items", data=data, follow_redirects=False) def create_subitem(client, item_id, name="SubItem A", note="Small", quantity="3"): return client.post( f"/items/{item_id}/subitems", data={"name": name, "note": note, "quantity": quantity}, follow_redirects=False, ) def test_app_uses_isolated_sqlite_database(client): assert db_module.engine is not None database_name = Path(db_module.engine.url.database) assert database_name.name == "test.db" assert "data/app.db" not in str(database_name) def test_root_redirects_to_boxes(client): response = client.get("/", follow_redirects=False) assert response.status_code == 302 assert response.headers["location"] == "/boxes" def test_boxes_page_returns_200(client): response = client.get("/boxes") assert response.status_code == 200 assert "箱子" in response.text def test_can_create_box(client, db_session): response = create_box(client, name="Kitchen Box") assert response.status_code == 303 box = db_session.query(Box).one() assert box.name == "Kitchen Box" assert box.room == "Bedroom" def test_can_edit_box(client, db_session): box = Box(name="Old Box") db_session.add(box) db_session.commit() response = client.post( f"/boxes/{box.id}/update", data={"name": "Updated Box", "note": "Updated", "room": "Office", "status": "open"}, follow_redirects=False, ) assert response.status_code == 303 db_session.refresh(box) assert box.name == "Updated Box" assert box.room == "Office" assert box.status == "open" def test_can_delete_box(client, db_session): box = Box(name="Delete Me") db_session.add(box) db_session.commit() box_id = box.id response = client.post(f"/boxes/{box_id}/delete", follow_redirects=False) assert response.status_code == 303 db_session.expire_all() assert db_session.get(Box, box_id) is None def test_deleting_box_cascades_to_items_and_subitems(client, db_session): box = Box(name="Cascade Box") item = Item(name="Container", is_container=True, box=box) subitem = SubItem(name="Cable", parent_item=item) db_session.add_all([box, item, subitem]) db_session.commit() response = client.post(f"/boxes/{box.id}/delete", follow_redirects=False) assert response.status_code == 303 assert db_session.query(Box).count() == 0 assert db_session.query(Item).count() == 0 assert db_session.query(SubItem).count() == 0 def test_missing_box_returns_404(client): response = client.get("/boxes/9999") assert response.status_code == 404 def test_box_detail_returns_200_when_box_exists(client, db_session): box = Box(name="Visible Box") db_session.add(box) db_session.commit() response = client.get(f"/boxes/{box.id}") assert response.status_code == 200 assert "Visible Box" in response.text def test_can_create_regular_item_under_box(client, db_session): box = Box(name="Main Box") db_session.add(box) db_session.commit() response = create_item(client, box.id, name="Book", is_container=False) assert response.status_code == 303 item = db_session.query(Item).one() assert item.name == "Book" assert item.is_container is False def test_can_create_container_item_under_box(client, db_session): box = Box(name="Main Box") db_session.add(box) db_session.commit() response = create_item(client, box.id, name="Accessory Pouch", is_container=True) assert response.status_code == 303 item = db_session.query(Item).one() assert item.is_container is True def test_can_edit_item(client, db_session): box = Box(name="Main Box") item = Item(name="Old Item", box=box, is_container=False) db_session.add_all([box, item]) db_session.commit() response = client.post( f"/items/{item.id}/update", data={"name": "New Item", "note": "Changed", "quantity": "7", "is_container": "on"}, follow_redirects=False, ) assert response.status_code == 303 db_session.refresh(item) assert item.name == "New Item" assert item.quantity == 7 assert item.is_container is True def test_can_delete_item(client, db_session): box = Box(name="Main Box") item = Item(name="Delete Item", box=box, is_container=False) db_session.add_all([box, item]) db_session.commit() item_id = item.id response = client.post(f"/items/{item_id}/delete", follow_redirects=False) assert response.status_code == 303 db_session.expire_all() assert db_session.get(Item, item_id) is None def test_deleting_container_item_cascades_to_subitems(client, db_session): box = Box(name="Main Box") item = Item(name="Container", box=box, is_container=True) subitem = SubItem(name="Clip", parent_item=item) db_session.add_all([box, item, subitem]) db_session.commit() response = client.post(f"/items/{item.id}/delete", follow_redirects=False) assert response.status_code == 303 assert db_session.query(Item).count() == 0 assert db_session.query(SubItem).count() == 0 def test_missing_item_returns_404(client): response = client.get("/items/9999") assert response.status_code == 404 def test_item_detail_returns_200_when_item_exists(client, db_session): box = Box(name="Main Box") item = Item(name="Visible Item", box=box, is_container=False) db_session.add_all([box, item]) db_session.commit() response = client.get(f"/items/{item.id}") assert response.status_code == 200 assert "Visible Item" in response.text def test_can_create_subitem_under_container_item(client, db_session): box = Box(name="Main Box") item = Item(name="Pouch", box=box, is_container=True) db_session.add_all([box, item]) db_session.commit() response = create_subitem(client, item.id, name="USB Cable") assert response.status_code == 303 subitem = db_session.query(SubItem).one() assert subitem.name == "USB Cable" def test_cannot_create_subitem_under_non_container_item(client, db_session): box = Box(name="Main Box") item = Item(name="Book", box=box, is_container=False) db_session.add_all([box, item]) db_session.commit() response = create_subitem(client, item.id, name="Should Fail") assert response.status_code == 400 assert response.json()["detail"] == "Only container items can have sub-items" assert db_session.query(SubItem).count() == 0 def test_can_edit_subitem(client, db_session): box = Box(name="Main Box") item = Item(name="Pouch", box=box, is_container=True) subitem = SubItem(name="Old Sub-item", parent_item=item) db_session.add_all([box, item, subitem]) db_session.commit() response = client.post( f"/subitems/{subitem.id}/update", data={"name": "New Sub-item", "note": "Updated", "quantity": "9"}, follow_redirects=False, ) assert response.status_code == 303 db_session.refresh(subitem) assert subitem.name == "New Sub-item" assert subitem.quantity == 9 def test_can_delete_subitem(client, db_session): box = Box(name="Main Box") item = Item(name="Pouch", box=box, is_container=True) subitem = SubItem(name="Delete Sub-item", parent_item=item) db_session.add_all([box, item, subitem]) db_session.commit() subitem_id = subitem.id response = client.post(f"/subitems/{subitem_id}/delete", follow_redirects=False) assert response.status_code == 303 db_session.expire_all() assert db_session.get(SubItem, subitem_id) is None def test_missing_subitem_returns_404(client): response = client.get("/subitems/9999/edit") assert response.status_code == 404 def test_post_redirects_are_reasonable(client, db_session): box = Box(name="Redirect Box") db_session.add(box) db_session.commit() item_response = create_item(client, box.id, name="Lamp") item_id = int(item_response.headers["location"].split("/")[-1]) item = db_session.get(Item, item_id) item.is_container = True db_session.commit() subitem_response = create_subitem(client, item.id, name="Bulb") assert item_response.headers["location"] == f"/items/{item.id}" assert subitem_response.headers["location"] == f"/items/{item.id}"