lllin000_PaperForge/tests/test_pdf_resolver.py

270 lines
10 KiB
Python

"""Tests for paperforge.pdf_resolver.
Covers absolute, vault-relative, junction, storage-relative, missing file,
and has_pdf=False scenarios.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from paperforge.pdf_resolver import is_valid_pdf, resolve_junction, resolve_pdf_path
class TestResolvePdfPath:
"""Tests for resolve_pdf_path resolution logic."""
def test_absolute_existing_path(self, tmp_path: Path) -> None:
"""Absolute path to existing PDF returns that path."""
pdf = tmp_path / "test.pdf"
pdf.write_text("PDF content")
result = resolve_pdf_path(str(pdf), True, tmp_path)
assert result == str(pdf.resolve())
def test_vault_relative_existing_path(self, tmp_path: Path) -> None:
"""Vault-relative path resolves against vault root."""
subdir = tmp_path / "storage" / "ABC123"
subdir.mkdir(parents=True)
pdf = subdir / "item.pdf"
pdf.write_text("PDF content")
rel_path = "storage/ABC123/item.pdf"
result = resolve_pdf_path(rel_path, True, tmp_path)
assert result == str(pdf.resolve())
def test_junction_path_mocked(self, tmp_path: Path, monkeypatch) -> None:
"""Junction path returns resolved target path (mocked for cross-platform)."""
target = tmp_path / "target.pdf"
target.write_text("PDF content")
junction = tmp_path / "junction.pdf"
def mock_resolve_junction(path: Path) -> Path:
if path == junction:
return target
return path
monkeypatch.setattr("paperforge.pdf_resolver.resolve_junction", mock_resolve_junction)
result = resolve_pdf_path(str(junction), True, tmp_path)
assert result == str(target.resolve())
def test_zotero_storage_relative_path(self, tmp_path: Path) -> None:
"""storage: prefixed path resolves against zotero_dir/storage/."""
zotero_dir = tmp_path / "zotero"
storage_dir = zotero_dir / "storage" / "ABC123"
storage_dir.mkdir(parents=True)
pdf = storage_dir / "item.pdf"
pdf.write_text("PDF content")
result = resolve_pdf_path("storage:ABC123/item.pdf", True, tmp_path, zotero_dir)
assert result == str(pdf.resolve())
def test_missing_file_returns_empty(self, tmp_path: Path) -> None:
"""Non-existent file returns empty string."""
result = resolve_pdf_path(str(tmp_path / "missing.pdf"), True, tmp_path)
assert result == ""
def test_has_pdf_false_returns_empty(self, tmp_path: Path) -> None:
"""has_pdf=False returns empty string regardless of path."""
pdf = tmp_path / "test.pdf"
pdf.write_text("PDF content")
result = resolve_pdf_path(str(pdf), False, tmp_path)
assert result == ""
def test_empty_path_returns_empty(self, tmp_path: Path) -> None:
"""Empty pdf_path returns empty string."""
result = resolve_pdf_path("", True, tmp_path)
assert result == ""
def test_whitespace_path_returns_empty(self, tmp_path: Path) -> None:
"""Whitespace-only pdf_path returns empty string."""
result = resolve_pdf_path(" ", True, tmp_path)
assert result == ""
class TestIsValidPdf:
"""Tests for is_valid_pdf helper."""
def test_existing_non_empty_file(self, tmp_path: Path) -> None:
"""Existing non-empty file returns True."""
pdf = tmp_path / "test.pdf"
pdf.write_text("PDF content")
assert is_valid_pdf(pdf) is True
def test_missing_file(self, tmp_path: Path) -> None:
"""Missing file returns False."""
assert is_valid_pdf(tmp_path / "missing.pdf") is False
def test_empty_file(self, tmp_path: Path) -> None:
"""Empty file (0 bytes) returns False."""
pdf = tmp_path / "empty.pdf"
pdf.write_text("")
assert is_valid_pdf(pdf) is False
def test_directory_not_file(self, tmp_path: Path) -> None:
"""Directory path returns False."""
subdir = tmp_path / "subdir"
subdir.mkdir()
assert is_valid_pdf(subdir) is False
class TestResolveJunction:
"""Tests for resolve_junction helper."""
def test_nonexistent_path_unchanged(self, tmp_path: Path) -> None:
"""Non-existent path is returned unchanged."""
missing = tmp_path / "missing.pdf"
assert resolve_junction(missing) == missing
def test_regular_file_unchanged(self, tmp_path: Path) -> None:
"""Regular file (not a junction) returns itself."""
pdf = tmp_path / "test.pdf"
pdf.write_text("PDF content")
result = resolve_junction(pdf)
assert result == pdf
def test_symlink_followed(self, tmp_path: Path) -> None:
"""Symbolic link to file is resolved to target."""
target = tmp_path / "target.pdf"
target.write_text("PDF content")
link = tmp_path / "link.pdf"
try:
link.symlink_to(target)
result = resolve_junction(link)
assert result == target
except OSError:
pytest.skip("Symlinks not supported on this platform")
def test_directory_junction_mocked(self, tmp_path: Path) -> None:
"""Directory junction resolution is platform-dependent; covered by symlink test above."""
pytest.skip("Windows junction mock incompatible with Python import semantics for `from ctypes import wintypes`")
class TestLoadExportRowsAttachmentNormalization:
"""Tests for load_export_rows() attachment path normalization.
Verifies that BBT-exported bare KEY/KEY.pdf paths are normalized to
storage:KEY/KEY.pdf format so resolve_pdf_path() can resolve them correctly.
"""
def test_bare_key_key_pdf_normalized_to_storage_prefix(self, tmp_path: Path) -> None:
"""Bare 'KEY/KEY.pdf' path is normalized to 'storage:KEY/KEY.pdf'."""
from paperforge.worker.sync import load_export_rows
export_data = {
"items": [
{
"key": "ABC123",
"itemKey": "ABC123",
"itemType": "journalArticle",
"title": "Test Paper",
"attachments": [{"path": "ABC123/ABC123.pdf", "contentType": "application/pdf"}],
}
],
"collections": {},
}
export_file = tmp_path / "library.json"
export_file.write_text(json.dumps(export_data), encoding="utf-8")
rows = load_export_rows(export_file)
assert len(rows) == 1
assert rows[0]["attachments"][0]["path"] == "storage:ABC123/ABC123.pdf"
def test_storage_prefix_preserved(self, tmp_path: Path) -> None:
"""Already-prefixed 'storage:KEY/KEY.pdf' path is not double-prefixed."""
from paperforge.worker.sync import load_export_rows
export_data = {
"items": [
{
"key": "ABC123",
"itemKey": "ABC123",
"itemType": "journalArticle",
"title": "Test Paper",
"attachments": [{"path": "storage:ABC123/ABC123.pdf", "contentType": "application/pdf"}],
}
],
"collections": {},
}
export_file = tmp_path / "library.json"
export_file.write_text(json.dumps(export_data), encoding="utf-8")
rows = load_export_rows(export_file)
assert len(rows) == 1
assert rows[0]["attachments"][0]["path"] == "storage:ABC123/ABC123.pdf"
def test_absolute_path_normalized_with_prefix(self, tmp_path: Path) -> None:
"""Absolute paths are normalized with absolute: prefix."""
from paperforge.worker.sync import load_export_rows
abs_path = str(tmp_path / "ABC123" / "ABC123.pdf")
export_data = {
"items": [
{
"key": "ABC123",
"itemKey": "ABC123",
"itemType": "journalArticle",
"title": "Test Paper",
"attachments": [{"path": abs_path, "contentType": "application/pdf"}],
}
],
"collections": {},
}
export_file = tmp_path / "library.json"
export_file.write_text(json.dumps(export_data), encoding="utf-8")
rows = load_export_rows(export_file)
assert len(rows) == 1
assert rows[0]["attachments"][0]["path"] == f"absolute:{abs_path}"
def test_empty_attachment_path_unchanged(self, tmp_path: Path) -> None:
"""Empty attachment path is returned unchanged."""
from paperforge.worker.sync import load_export_rows
export_data = {
"items": [
{
"key": "ABC123",
"itemKey": "ABC123",
"itemType": "journalArticle",
"title": "Test Paper",
"attachments": [{"path": "", "contentType": ""}],
}
],
"collections": {},
}
export_file = tmp_path / "library.json"
export_file.write_text(json.dumps(export_data), encoding="utf-8")
rows = load_export_rows(export_file)
assert len(rows) == 1
assert rows[0]["attachments"][0]["path"] == ""
def test_non_pdf_attachment_unchanged(self, tmp_path: Path) -> None:
"""Non-PDF attachments are returned with empty contentType and unchanged path."""
from paperforge.worker.sync import load_export_rows
export_data = {
"items": [
{
"key": "ABC123",
"itemKey": "ABC123",
"itemType": "journalArticle",
"title": "Test Paper",
"attachments": [{"path": "ABC123/ABC123.docx", "contentType": ""}],
}
],
"collections": {},
}
export_file = tmp_path / "library.json"
export_file.write_text(json.dumps(export_data), encoding="utf-8")
rows = load_export_rows(export_file)
assert len(rows) == 1
assert rows[0]["attachments"][0]["path"] == "storage:ABC123/ABC123.docx"
assert rows[0]["attachments"][0]["contentType"] == ""