"""Tests for tagsync.core: state DB + run_sync main loop. Dropbox client calls are monkeypatched via the `patched_core` fixture, so these tests don't require network or the dropbox SDK to be installed. """ from __future__ import annotations from pathlib import Path import pytest from tagsync import core from tests.conftest import FakeFileMetadata # --- State DB --------------------------------------------------------------- def test_open_state_creates_schema(tmp_path): db = tmp_path / "state.db" conn = core.open_state(db) try: tables = [ row[0] for row in conn.execute( "SELECT name FROM sqlite_master WHERE type='table'" ) ] finally: conn.close() assert "file_tags" in tables def test_open_state_is_idempotent(tmp_path): db = tmp_path / "state.db" core.open_state(db).close() # Re-opening an existing DB should not raise. core.open_state(db).close() def test_open_state_creates_parent_dir(tmp_path): db = tmp_path / "nested" / "dir" / "state.db" conn = core.open_state(db) conn.close() assert db.exists() # --- SyncStats -------------------------------------------------------------- def test_sync_stats_summary_format(): s = core.SyncStats(applied=3, cleared=1, errors=0) summary = s.summary() for key in ("applied", "cleared", "skipped", "missing", "errors"): assert key in summary # --- Writer implementations used by run_sync -------------------------------- class RecordingWriter: """Fake Writer that records every call for assertions.""" def __init__( self, *, supports_all: bool = True, online_paths: set[str] | None = None, fail_paths: set[str] | None = None, ): self.supports_all = supports_all self.online_paths = online_paths or set() self.fail_paths = fail_paths or set() self.written: list[tuple[Path, list[str]]] = [] def supports(self, path: Path) -> bool: return self.supports_all def is_online_only(self, path: Path) -> bool: return str(path) in self.online_paths def write_tags(self, path: Path, tags: list[str]) -> None: if str(path) in self.fail_paths: raise RuntimeError("simulated tag failure") self.written.append((path, list(tags))) # --- run_sync happy paths --------------------------------------------------- def _make_local_file(local_root: Path, rel: str) -> Path: p = local_root / rel p.parent.mkdir(parents=True, exist_ok=True) p.touch() return p def test_run_sync_applies_new_tags(tmp_path, patched_core): local_root = tmp_path / "Dropbox" local_root.mkdir() _make_local_file(local_root, "Report.pdf") patched_core["files"] = [ FakeFileMetadata(name="Report.pdf", path_lower="/report.pdf", path_display="/Report.pdf"), ] patched_core["tags_by_path"] = {"/report.pdf": ["urgent", "client"]} writer = RecordingWriter() stats = core.run_sync( token="fake", local_root=local_root, state_db=tmp_path / "state.db", writer=writer, ) assert stats.applied == 1 assert stats.errors == 0 assert writer.written == [ (local_root / "Report.pdf", ["client", "urgent"]), # sorted ] def test_run_sync_skips_unchanged_tags(tmp_path, patched_core): local_root = tmp_path / "Dropbox" local_root.mkdir() _make_local_file(local_root, "a.pdf") patched_core["files"] = [ FakeFileMetadata("a.pdf", "/a.pdf", "/a.pdf") ] patched_core["tags_by_path"] = {"/a.pdf": ["x"]} writer = RecordingWriter() state_db = tmp_path / "state.db" first = core.run_sync("t", local_root, state_db, writer) assert first.applied == 1 # Second run with the same Dropbox state should skip. second = core.run_sync("t", local_root, state_db, writer) assert second.applied == 0 assert second.skipped == 1 assert len(writer.written) == 1 # unchanged from first run def test_run_sync_clears_tags_when_removed_upstream(tmp_path, patched_core): local_root = tmp_path / "Dropbox" local_root.mkdir() _make_local_file(local_root, "b.pdf") patched_core["files"] = [FakeFileMetadata("b.pdf", "/b.pdf", "/b.pdf")] patched_core["tags_by_path"] = {"/b.pdf": ["temp"]} writer = RecordingWriter() state_db = tmp_path / "state.db" core.run_sync("t", local_root, state_db, writer) # Upstream removes the tag. patched_core["tags_by_path"] = {"/b.pdf": []} stats = core.run_sync("t", local_root, state_db, writer) assert stats.cleared == 1 assert writer.written[-1] == (local_root / "b.pdf", []) def test_run_sync_skips_missing_local_files(tmp_path, patched_core): local_root = tmp_path / "Dropbox" local_root.mkdir() # Don't create the local file. patched_core["files"] = [FakeFileMetadata("c.pdf", "/c.pdf", "/c.pdf")] patched_core["tags_by_path"] = {"/c.pdf": ["x"]} writer = RecordingWriter() stats = core.run_sync( "t", local_root, tmp_path / "state.db", writer ) assert stats.missing == 1 assert writer.written == [] def test_run_sync_respects_unsupported_files(tmp_path, patched_core): local_root = tmp_path / "Dropbox" local_root.mkdir() _make_local_file(local_root, "data.zip") patched_core["files"] = [ FakeFileMetadata("data.zip", "/data.zip", "/data.zip") ] patched_core["tags_by_path"] = {"/data.zip": ["archive"]} writer = RecordingWriter(supports_all=False) stats = core.run_sync( "t", local_root, tmp_path / "state.db", writer ) assert stats.unsupported == 1 assert stats.applied == 0 def test_run_sync_skips_online_only_files_when_requested(tmp_path, patched_core): local_root = tmp_path / "Dropbox" local_root.mkdir() local_file = _make_local_file(local_root, "cloud.pdf") patched_core["files"] = [FakeFileMetadata("cloud.pdf", "/cloud.pdf", "/cloud.pdf")] patched_core["tags_by_path"] = {"/cloud.pdf": ["tag"]} writer = RecordingWriter(online_paths={str(local_file)}) stats = core.run_sync( "t", local_root, tmp_path / "state.db", writer, skip_online_only=True, ) assert stats.online_only == 1 assert stats.applied == 0 def test_run_sync_dry_run_touches_nothing(tmp_path, patched_core): local_root = tmp_path / "Dropbox" local_root.mkdir() _make_local_file(local_root, "d.pdf") patched_core["files"] = [FakeFileMetadata("d.pdf", "/d.pdf", "/d.pdf")] patched_core["tags_by_path"] = {"/d.pdf": ["x"]} writer = RecordingWriter() stats = core.run_sync( "t", local_root, tmp_path / "state.db", writer, dry_run=True, ) assert stats.applied == 1 assert writer.written == [] # nothing actually written def test_run_sync_counts_writer_errors(tmp_path, patched_core): local_root = tmp_path / "Dropbox" local_root.mkdir() local_file = _make_local_file(local_root, "e.pdf") patched_core["files"] = [FakeFileMetadata("e.pdf", "/e.pdf", "/e.pdf")] patched_core["tags_by_path"] = {"/e.pdf": ["x"]} writer = RecordingWriter(fail_paths={str(local_file)}) stats = core.run_sync( "t", local_root, tmp_path / "state.db", writer, ) assert stats.errors == 1 assert stats.applied == 0 # --- Path translation ------------------------------------------------------- def test_path_display_to_local(tmp_path, patched_core): """Dropbox's /A/B/C.pdf must map under the configured local root.""" local_root = tmp_path / "Dropbox" local_root.mkdir() _make_local_file(local_root, "A/B/C.pdf") patched_core["files"] = [ FakeFileMetadata("C.pdf", "/a/b/c.pdf", "/A/B/C.pdf") ] patched_core["tags_by_path"] = {"/a/b/c.pdf": ["t"]} writer = RecordingWriter() core.run_sync( "t", local_root, tmp_path / "state.db", writer, ) assert writer.written written_path, _ = writer.written[0] assert written_path == local_root / "A" / "B" / "C.pdf"