2026-04-19 12:13:07 +02:00
|
|
|
from pathlib import Path
|
|
|
|
|
|
2026-04-19 12:36:55 +02:00
|
|
|
import app.db as db_module
|
|
|
|
|
from app.models import Box, Item, SubItem
|
2026-04-19 12:13:07 +02:00
|
|
|
|
|
|
|
|
|
2026-04-19 12:36:55 +02:00
|
|
|
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,
|
|
|
|
|
)
|
2026-04-19 12:13:07 +02:00
|
|
|
|
2026-04-19 12:36:55 +02:00
|
|
|
|
|
|
|
|
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)
|
2026-04-19 12:13:07 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-04-19 12:36:55 +02:00
|
|
|
assert "箱子" in response.text
|
2026-04-19 12:13:07 +02:00
|
|
|
|
|
|
|
|
|
2026-04-19 12:36:55 +02:00
|
|
|
def test_can_create_box(client, db_session):
|
|
|
|
|
response = create_box(client, name="Kitchen Box")
|
2026-04-19 12:13:07 +02:00
|
|
|
|
2026-04-19 12:36:55 +02:00
|
|
|
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}"
|