test_preview_service.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. """PreviewService 单元测试,不依赖数据库或真实 LibreOffice。"""
  2. from __future__ import annotations
  3. from pathlib import Path
  4. import pytest
  5. from dms.services.document_converter import FakeDocumentConverter
  6. from dms.services.preview_service import PreviewService, preview_cache_key
  7. from dms.storage.paths import UnsafeStoragePathError, preview_cache_path
  8. @pytest.fixture
  9. def storage_root(tmp_path: Path) -> Path:
  10. root = tmp_path / "dms-storage"
  11. (root / "preview").mkdir(parents=True)
  12. (root / "original").mkdir(parents=True)
  13. return root
  14. @pytest.fixture
  15. def preview_service(storage_root: Path) -> PreviewService:
  16. return PreviewService(storage_root, FakeDocumentConverter())
  17. def test_preview_cache_path_with_hash_prefix(storage_root: Path) -> None:
  18. path = preview_cache_path("abcdef123456", storage_root)
  19. assert path.name == "abcdef123456.pdf"
  20. assert path.parent.name == "ab"
  21. assert path.parent.parent.name == "preview"
  22. def test_preview_cache_path_rejects_invalid_hash(storage_root: Path) -> None:
  23. with pytest.raises(UnsafeStoragePathError):
  24. preview_cache_path("", storage_root)
  25. def test_validate_cached_pdf_accepts_valid_pdf(
  26. preview_service: PreviewService, storage_root: Path
  27. ) -> None:
  28. cache = preview_cache_path("validhash", storage_root)
  29. cache.parent.mkdir(parents=True, exist_ok=True)
  30. # 生成 fitz 可真正打开的合法最小 PDF,避免仅写入魔数导致验证失败
  31. import fitz
  32. doc = fitz.open()
  33. doc.new_page()
  34. doc.save(str(cache))
  35. doc.close()
  36. assert preview_service.validate_cached_pdf(cache) is True
  37. def test_validate_cached_pdf_rejects_non_pdf(
  38. preview_service: PreviewService, storage_root: Path
  39. ) -> None:
  40. cache = preview_cache_path("nonpdfhash", storage_root)
  41. cache.parent.mkdir(parents=True, exist_ok=True)
  42. cache.write_bytes(b"not a pdf")
  43. assert preview_service.validate_cached_pdf(cache) is False
  44. def test_validate_cached_pdf_rejects_path_outside_preview(
  45. preview_service: PreviewService, tmp_path: Path
  46. ) -> None:
  47. outside = tmp_path / "outside.pdf"
  48. outside.write_bytes(b"%PDF-1.4")
  49. assert preview_service.validate_cached_pdf(outside) is False
  50. class FakeDocument:
  51. def __init__(
  52. self,
  53. file_extension: str,
  54. file_hash: str,
  55. file_relative_path: str,
  56. ) -> None:
  57. self.file_extension = file_extension
  58. self.file_hash = file_hash
  59. self.file_relative_path = file_relative_path
  60. def test_preview_path_for_pdf_returns_original(
  61. preview_service: PreviewService, storage_root: Path
  62. ) -> None:
  63. original = storage_root / "original" / "sample.pdf"
  64. original.write_bytes(b"%PDF-1.4")
  65. document = FakeDocument("pdf", "pdfhash", "original/sample.pdf")
  66. path = preview_service.preview_path_for(document)
  67. assert path == original
  68. def test_preview_path_for_docx_generates_cache(
  69. preview_service: PreviewService, storage_root: Path
  70. ) -> None:
  71. original = storage_root / "original" / "sample.docx"
  72. original.write_bytes(b"fake docx content")
  73. document = FakeDocument("docx", "docxhash", "original/sample.docx")
  74. path = preview_service.preview_path_for(document)
  75. assert path.name == "docxhash.pdf"
  76. assert path.exists()
  77. assert path.read_bytes().startswith(b"%PDF-")
  78. def test_preview_path_for_wps_generates_cache(
  79. preview_service: PreviewService, storage_root: Path
  80. ) -> None:
  81. original = storage_root / "original" / "sample.wps"
  82. original.write_bytes(b"fake wps content")
  83. document = FakeDocument("wps", "wpshash", "original/sample.wps")
  84. path = preview_service.preview_path_for(document)
  85. assert path.name == "wpshash.pdf"
  86. assert path.exists()
  87. assert path.read_bytes().startswith(b"%PDF-")
  88. def test_preview_path_for_doc_uses_cache_on_second_call(
  89. preview_service: PreviewService, storage_root: Path
  90. ) -> None:
  91. original = storage_root / "original" / "sample.doc"
  92. original.write_bytes(b"fake doc content")
  93. document = FakeDocument("doc", "dochash", "original/sample.doc")
  94. first = preview_service.preview_path_for(document)
  95. mtime = first.stat().st_mtime
  96. second = preview_service.preview_path_for(document)
  97. assert second == first
  98. assert second.stat().st_mtime == mtime
  99. def test_preview_cache_key_spreadsheet_gets_version_suffix() -> None:
  100. assert preview_cache_key("abcdef123456", "xlsx", True) == "abcdef123456-sps2"
  101. assert preview_cache_key("abcdef123456", "xlsx", False) == "abcdef123456-sps0"
  102. assert preview_cache_key("abcdef123456", "XLS", True) == "abcdef123456-sps2"
  103. assert preview_cache_key("abcdef123456", ".et", False) == "abcdef123456-sps0"
  104. assert preview_cache_key("abcdef123456", "docx", True) == "abcdef123456"
  105. assert preview_cache_key("abcdef123456", "wps", False) == "abcdef123456"
  106. def test_preview_path_for_xlsx_uses_suffixed_cache(
  107. preview_service: PreviewService, storage_root: Path
  108. ) -> None:
  109. original = storage_root / "original" / "sample.xlsx"
  110. original.write_bytes(b"fake xlsx content")
  111. document = FakeDocument("xlsx", "xlshash", "original/sample.xlsx")
  112. path = preview_service.preview_path_for(document)
  113. # 表格类缓存键带策略版本后缀(Fake 转换器不支持单页 → 分页键 sps0)
  114. assert path.name == "xlshash-sps0.pdf"
  115. assert path.exists()
  116. assert path.read_bytes().startswith(b"%PDF-")
  117. def test_preview_reuses_valid_cache_when_replace_occupied(
  118. preview_service: PreviewService,
  119. storage_root: Path,
  120. monkeypatch: pytest.MonkeyPatch,
  121. ) -> None:
  122. """并发场景:os.replace 因目标被先完成方读取占用而失败时,幂等复用有效缓存。"""
  123. import fitz
  124. import os as os_module
  125. original = storage_root / "original" / "sample.docx"
  126. original.write_bytes(b"fake docx content")
  127. document = FakeDocument("docx", "occupiedhash", "original/sample.docx")
  128. cache = preview_cache_path("occupiedhash", storage_root)
  129. # 预置无效缓存,确保前置校验不命中、走转换写缓存路径
  130. cache.parent.mkdir(parents=True, exist_ok=True)
  131. cache.write_bytes(b"not a pdf")
  132. def fake_replace(src: Path, dst: Path) -> None:
  133. # 模拟并发先完成方:已写好有效缓存且目标正被其读取
  134. # (Windows 下共享模式不含删除权限 → 替换失败抛 PermissionError)
  135. doc = fitz.open()
  136. doc.new_page()
  137. doc.save(str(dst))
  138. doc.close()
  139. raise PermissionError(13, "另一个程序正在使用此文件,进程无法访问")
  140. monkeypatch.setattr(os_module, "replace", fake_replace)
  141. path = preview_service.preview_path_for(document)
  142. assert path == cache
  143. assert preview_service.validate_cached_pdf(cache) is True
  144. # 临时文件已清理,不残留 .tmp
  145. assert list(cache.parent.glob("*.tmp")) == []