Files
stack/tests/bib/test_sync_deeper.py
kert e204ea0e4c feat(bib/sync): push bib notes into Zotero itemNotes
Mirrors _sync_attachments. Idempotent dedup keyed on
(parentItemID, title) so re-syncing is a no-op. Wired into both
the create-item and update-existing-item branches of push_to_zotero.
Adds 'notes' counter to the stats dict.

Fixes test_sync_deeper mocks: both _sync_attachments and _sync_notes
call store._con().execute(); updated the MagicMock side_effect to
route by SQL fragment so the notes query returns [] while attachment
queries return the fixture rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 09:37:12 -04:00

284 lines
9.1 KiB
Python

"""Exercise bib.sync — push_to_zotero with real Zotero DB + mocked store."""
from __future__ import annotations
from unittest.mock import MagicMock
from bib.item import Manual, Source
from bib.sync import _sync_attachments, _zotero_collection_path, push_to_zotero
from zot.db import TYPE_MAP, Db
from zot.schema import create_db
def _setup(tmp_path):
path = str(tmp_path / "z.sqlite")
con = create_db(path)
con.close()
return path
class TestSyncAttachments:
def test_no_attachments(self, tmp_path):
path = _setup(tmp_path)
store = MagicMock()
store._con.return_value.execute.return_value.fetchall.return_value = []
with Db(path) as db:
parent_id = db.create_item(TYPE_MAP["document"])
db.commit()
n = _sync_attachments(
db, store, MagicMock(key="K1"), parent_id, tmp_path / "storage"
)
assert n == 0
def test_with_attachment(self, tmp_path):
path = _setup(tmp_path)
storage = tmp_path / "storage"
storage.mkdir()
# Create a fake source file
src_file = tmp_path / "source" / "paper.pdf"
src_file.parent.mkdir()
src_file.write_bytes(b"%PDF content")
store = MagicMock()
store._con.return_value.execute.return_value.fetchall.return_value = [
{
"filename": "paper.pdf",
"content_type": "application/pdf",
"storage_path": str(src_file),
},
]
bib_item = MagicMock(key="K1")
with Db(path) as db:
parent_id = db.create_item(TYPE_MAP["document"])
db.commit()
n = _sync_attachments(db, store, bib_item, parent_id, storage)
db.commit()
assert n == 1
def test_dedup(self, tmp_path):
path = _setup(tmp_path)
storage = tmp_path / "storage"
storage.mkdir()
src_file = tmp_path / "source" / "paper.pdf"
src_file.parent.mkdir()
src_file.write_bytes(b"%PDF content")
store = MagicMock()
store._con.return_value.execute.return_value.fetchall.return_value = [
{
"filename": "paper.pdf",
"content_type": "application/pdf",
"storage_path": str(src_file),
},
]
bib_item = MagicMock(key="K1")
with Db(path) as db:
parent_id = db.create_item(TYPE_MAP["document"])
db.commit()
# First attach
n1 = _sync_attachments(db, store, bib_item, parent_id, storage)
db.commit()
# Second attach should skip (dedup)
n2 = _sync_attachments(db, store, bib_item, parent_id, storage)
assert n1 == 1
assert n2 == 0
def test_missing_file(self, tmp_path):
path = _setup(tmp_path)
store = MagicMock()
store._con.return_value.execute.return_value.fetchall.return_value = [
{
"filename": "missing.pdf",
"content_type": "application/pdf",
"storage_path": "/nonexistent/file.pdf",
},
]
with Db(path) as db:
parent_id = db.create_item(TYPE_MAP["document"])
db.commit()
n = _sync_attachments(
db, store, MagicMock(key="K1"), parent_id, tmp_path / "storage"
)
assert n == 0
class TestPushToZotero:
def test_creates_source(self, tmp_path):
path = _setup(tmp_path)
item = Source(title="Test Document", url="https://example.com/doc")
item.doc_type = "Public Comment"
item.add_tag("source:regulations-gov")
stats = push_to_zotero([item], zotero_db=path)
assert stats["created"] >= 1
def test_skips_existing_url(self, tmp_path):
path = _setup(tmp_path)
item = Source(title="Test", url="https://example.com/doc")
item.doc_type = "Public Comment"
# First push creates
push_to_zotero([item], zotero_db=path)
# Second push skips
stats = push_to_zotero([item], zotero_db=path)
assert stats["skipped"] >= 1
def test_collection_routing(self, tmp_path):
path = _setup(tmp_path)
item = Manual(title="IOM Ch1", manual_name="Claims Processing")
item.add_tag("source:iom")
stats = push_to_zotero([item], zotero_db=path)
assert stats["collections"] >= 1
def test_with_store_attachments(self, tmp_path):
path = _setup(tmp_path)
storage = tmp_path / "storage"
storage.mkdir()
src = tmp_path / "src" / "paper.pdf"
src.parent.mkdir()
src.write_bytes(b"%PDF content")
att_rows = [
{
"filename": "paper.pdf",
"content_type": "application/pdf",
"storage_path": str(src),
},
]
def _execute(sql, *args, **kwargs):
m = MagicMock()
# Notes query selects n.title; attachment query selects a.filename
m.fetchall.return_value = att_rows if "a.filename" in sql else []
return m
store = MagicMock()
store._con.return_value.execute.side_effect = _execute
item = Source(title="Test", url="https://unique-url-test.com/doc")
item.doc_type = "Public Comment"
stats = push_to_zotero(
[item], zotero_db=path, zotero_storage=str(storage), store=store
)
assert stats["created"] >= 1
class TestCollectionPathEdge:
def test_oig_with_guidance(self):
item = Source(title="SFA")
item.add_tag("agency:oig")
item.add_tag("guidance:sfa")
path = _zotero_collection_path(item)
assert "SFA" in path[-1]
def test_email_no_mailbox(self):
item = Source(title="Msg")
item.add_tag("source:email")
path = _zotero_collection_path(item)
assert path == ["Inbox"]
def test_oig_with_sector(self):
"""Line 421: OIG item with sector tag."""
item = Source(title="CPG")
item.add_tag("agency:oig")
item.add_tag("sector:hospitals")
path = _zotero_collection_path(item)
assert "Hospitals" in path[-1]
def test_oig_no_sector_no_guidance(self):
"""Line 421: OIG with neither sector nor guidance → base only."""
item = Source(title="OIG doc")
item.add_tag("agency:oig")
path = _zotero_collection_path(item)
assert path == ["Healthcare Data Platform", "OIG Guidance"]
class TestPushToZoteroCreators:
def test_journal_article_creators(self, tmp_path):
"""Lines 338, 339: creators are added for journal articles."""
path = _setup(tmp_path)
item = Source(
title="Test Article",
url="https://example.com/article",
doc_type="journal-article",
extra="Authors: Smith J; Jones K\nDOI: 10.1/x\nJournal: J\n",
date_published="2024-01-01",
)
stats = push_to_zotero([item], zotero_db=path)
assert stats["created"] >= 1
assert stats.get("creators", 0) >= 2
class TestPushToZoteroExistingWithAttachments:
def test_existing_url_syncs_attachments(self, tmp_path):
"""Lines 301, 312-314: existing URL item syncs attachments + collection path."""
path = _setup(tmp_path)
storage = tmp_path / "storage"
storage.mkdir()
src = tmp_path / "src" / "paper.pdf"
src.parent.mkdir()
src.write_bytes(b"%PDF content")
att_rows = [
{
"filename": "paper.pdf",
"content_type": "application/pdf",
"storage_path": str(src),
},
]
def _execute(sql, *args, **kwargs):
m = MagicMock()
# Notes query selects n.title; attachment query selects a.filename
m.fetchall.return_value = att_rows if "a.filename" in sql else []
return m
store = MagicMock()
store._con.return_value.execute.side_effect = _execute
item = Manual(
title="IOM Ch1",
url="https://example.com/iom-ch1",
manual_name="Claims Processing",
)
item.add_tag("source:iom")
# First push creates
stats1 = push_to_zotero(
[item], zotero_db=path, zotero_storage=str(storage), store=store
)
assert stats1["created"] >= 1
# Second push hits the existing-URL path (skips)
stats2 = push_to_zotero(
[item], zotero_db=path, zotero_storage=str(storage), store=store
)
assert stats2["skipped"] >= 1
class TestPushToZoteroCollectionPath:
def test_email_collection_path(self, tmp_path):
"""Lines 273: _resolve_path is cached across items."""
path = _setup(tmp_path)
item1 = Source(title="Email 1", url="https://x.com/e1")
item1.add_tag("source:email")
item1.add_tag("mailbox:updates")
item2 = Source(title="Email 2", url="https://x.com/e2")
item2.add_tag("source:email")
item2.add_tag("mailbox:updates")
stats = push_to_zotero([item1, item2], zotero_db=path)
assert stats["created"] >= 2