from app.models import Box, Item, SubItem from app.notion_import import ( ImportSummary, ParsedBox, ParsedItem, ParsedSubItem, apply_import, extract_page_id, parse_notion_blocks, ) def make_heading_2(text: str) -> dict: return { "type": "heading_2", "heading_2": {"rich_text": [{"plain_text": text}]}, "_children": [], } def make_bullet(text: str, children: list[dict] | None = None) -> dict: return { "type": "bulleted_list_item", "bulleted_list_item": {"rich_text": [{"plain_text": text}]}, "_children": children or [], } def make_image_block() -> dict: return {"type": "image", "image": {}, "_children": []} def test_extract_page_id_from_notion_url(): url = "https://www.notion.so/workspace/My-Page-1234567890abcdef1234567890abcdef?pvs=4" page_id = extract_page_id(url) assert page_id == "12345678-90ab-cdef-1234-567890abcdef" def test_parse_heading_2_as_box(): summary = parse_notion_blocks([make_heading_2("厨房箱")]) assert summary.box_count == 1 assert summary.boxes[0].name == "厨房箱" def test_parse_first_level_bullet_as_item(): blocks = [make_heading_2("客厅箱"), make_bullet("锅具")] summary = parse_notion_blocks(blocks) assert summary.item_count == 1 assert summary.boxes[0].items[0].name == "锅具" assert summary.boxes[0].items[0].is_container is False def test_parse_bullet_with_children_as_container_item_and_subitems(): blocks = [ make_heading_2("电子箱"), make_bullet("配件盒", children=[make_bullet("USB 线"), make_bullet("转接头")]), ] summary = parse_notion_blocks(blocks) item = summary.boxes[0].items[0] assert item.name == "配件盒" assert item.is_container is True assert [subitem.name for subitem in item.subitems] == ["USB 线", "转接头"] def test_parse_second_level_bullets_as_subitems(): blocks = [ make_heading_2("文件箱"), make_bullet("文件袋", children=[make_bullet("合同"), make_bullet("护照复印件")]), ] summary = parse_notion_blocks(blocks) assert summary.subitem_count == 2 assert summary.boxes[0].items[0].subitems[1].name == "护照复印件" def test_parse_deeper_than_supported_levels_adds_warning(): blocks = [ make_heading_2("测试箱"), make_bullet( "外层袋", children=[make_bullet("内层物品", children=[make_bullet("更深一层")])], ), ] summary = parse_notion_blocks(blocks) assert summary.container_item_count == 1 assert any("超出支持层级" in warning for warning in summary.warnings) def test_parse_non_text_media_block_adds_skip_warning(): blocks = [make_heading_2("照片箱"), make_image_block()] summary = parse_notion_blocks(blocks) assert any("这版不导入图片或媒体" in warning for warning in summary.warnings) def test_dry_run_parse_does_not_write_database(db_session): blocks = [make_heading_2("厨房箱"), make_bullet("锅")] summary = parse_notion_blocks(blocks) assert summary.box_count == 1 assert db_session.query(Box).count() == 0 assert db_session.query(Item).count() == 0 assert db_session.query(SubItem).count() == 0 def test_apply_import_writes_expected_structure(db_session): summary = ImportSummary( boxes=[ ParsedBox( name="主卧箱", items=[ ParsedItem(name="衣服", is_container=False), ParsedItem( name="收纳袋", is_container=True, subitems=[ParsedSubItem(name="袜子"), ParsedSubItem(name="围巾")], ), ], ) ] ) counts = apply_import(summary, db_session) assert counts == {"boxes": 1, "items": 2, "subitems": 2} assert db_session.query(Box).count() == 1 assert db_session.query(Item).count() == 2 assert db_session.query(SubItem).count() == 2 container_item = db_session.query(Item).filter_by(name="收纳袋").one() assert container_item.is_container is True