""" Unit tests for Local Sync Plugin. @testCovers fastapi_app/plugins/local_sync/plugin.py """ import unittest from unittest.mock import Mock, patch, MagicMock from pathlib import Path import tempfile import shutil class TestLocalSyncPlugin(unittest.TestCase): def setUp(self): """Set up test fixtures.""" self.temp_dir = tempfile.mkdtemp() def tearDown(self): """Clean up test fixtures.""" shutil.rmtree(self.temp_dir) def test_scan_filesystem(self): """Test filesystem scanning for TEI files.""" # Create test files test_path = Path(self.temp_dir) (test_path / "doc1.tei.xml").write_text("test1") (test_path / "subdir").mkdir() (test_path / "subdir" / "doc2.tei.xml").write_text("test2") (test_path / "ignore.xml").write_text("ignore") from fastapi_app.plugins.local_sync.plugin import LocalSyncPlugin plugin = LocalSyncPlugin() docs = plugin._scan_filesystem(test_path) self.assertEqual(len(docs), 2) self.assertTrue(any("doc1.tei.xml" in str(p) for p in docs.keys())) self.assertTrue(any("doc2.tei.xml" in str(p) for p in docs.keys())) def test_scan_filesystem_with_include_filter(self): """Test filesystem scanning with include pattern.""" # Create test files test_path = Path(self.temp_dir) (test_path / "doc1.tei.xml").write_text("test1") (test_path / "subdir").mkdir() (test_path / "subdir" / "doc2.tei.xml").write_text("test2") (test_path / "other.tei.xml").write_text("other") from fastapi_app.plugins.local_sync.plugin import LocalSyncPlugin plugin = LocalSyncPlugin() # Only include files in subdir docs = plugin._scan_filesystem(test_path, include_pattern=r"subdir") self.assertEqual(len(docs), 1) self.assertTrue(any("doc2.tei.xml" in str(p) for p in docs.keys())) def test_scan_filesystem_with_exclude_filter(self): """Test filesystem scanning with exclude pattern.""" # Create test files test_path = Path(self.temp_dir) (test_path / "doc1.tei.xml").write_text("test1") (test_path / "subdir").mkdir() (test_path / "subdir" / "doc2.tei.xml").write_text("test2") (test_path / "other.tei.xml").write_text("other") from fastapi_app.plugins.local_sync.plugin import LocalSyncPlugin plugin = LocalSyncPlugin() # Exclude files in subdir docs = plugin._scan_filesystem(test_path, exclude_pattern=r"subdir") self.assertEqual(len(docs), 2) self.assertTrue(any("doc1.tei.xml" in str(p) for p in docs.keys())) self.assertTrue(any("other.tei.xml" in str(p) for p in docs.keys())) def test_scan_filesystem_with_include_and_exclude_filters(self): """Test filesystem scanning with both include and exclude patterns.""" # Create test files test_path = Path(self.temp_dir) (test_path / "gold").mkdir() (test_path / "gold" / "doc1.tei.xml").write_text("test1") (test_path / "gold" / "doc2.tei.xml").write_text("test2") (test_path / "gold" / "draft.tei.xml").write_text("draft") (test_path / "draft").mkdir() (test_path / "draft" / "doc3.tei.xml").write_text("test3") from fastapi_app.plugins.local_sync.plugin import LocalSyncPlugin plugin = LocalSyncPlugin() # Include only files in gold, exclude files with "draft" in name docs = plugin._scan_filesystem(test_path, include_pattern=r"gold", exclude_pattern=r"draft") self.assertEqual(len(docs), 2) self.assertTrue(any("doc1.tei.xml" in str(p) for p in docs.keys())) self.assertTrue(any("doc2.tei.xml" in str(p) for p in docs.keys())) self.assertFalse(any("draft.tei.xml" in str(p) for p in docs.keys())) def test_scan_filesystem_with_filename_pattern(self): """Test filesystem scanning with filename-based pattern.""" # Create test files test_path = Path(self.temp_dir) (test_path / "article-001.tei.xml").write_text("test1") (test_path / "article-002.tei.xml").write_text("test2") (test_path / "review-001.tei.xml").write_text("review") from fastapi_app.plugins.local_sync.plugin import LocalSyncPlugin plugin = LocalSyncPlugin() # Only include files starting with "article" docs = plugin._scan_filesystem(test_path, include_pattern=r"article-\d+\.tei\.xml") self.assertEqual(len(docs), 2) self.assertTrue(any("article-001.tei.xml" in str(p) for p in docs.keys())) self.assertTrue(any("article-002.tei.xml" in str(p) for p in docs.keys())) self.assertFalse(any("review-001.tei.xml" in str(p) for p in docs.keys())) def test_extract_fileref_from_xml_id(self): """Test fileref extraction from fileDesc/@xml:id (primary path).""" tei_content = b""" Test """ from fastapi_app.lib.utils.tei_utils import extract_fileref fileref = extract_fileref(tei_content) self.assertEqual(fileref, "10.5771__2699-1284-2024-3-149") def test_extract_fileref_legacy_fallback(self): """Test fileref extraction from deprecated editionStmt/idno[@type='fileref'] fallback.""" tei_content = b""" test-doc-123 """ from fastapi_app.lib.utils.tei_utils import extract_fileref fileref = extract_fileref(tei_content) self.assertEqual(fileref, "test-doc-123") def test_extract_fileref_no_match(self): """Test fileref extraction with missing element.""" tei_content = b""" Test """ from fastapi_app.lib.utils.tei_utils import extract_fileref fileref = extract_fileref(tei_content) self.assertIsNone(fileref) def test_extract_timestamp(self): """Test timestamp extraction from last revision change using tei_utils.""" tei_content = b""" First change Latest change """ from fastapi_app.lib.utils.tei_utils import extract_revision_timestamp timestamp = extract_revision_timestamp(tei_content) self.assertEqual(timestamp, "2025-01-08T15:30:00") def test_extract_timestamp_no_changes(self): """Test timestamp extraction with no changes.""" tei_content = b""" Test """ from fastapi_app.lib.utils.tei_utils import extract_revision_timestamp timestamp = extract_revision_timestamp(tei_content) self.assertIsNone(timestamp) @patch('fastapi_app.lib.plugins.plugin_tools.get_plugin_config') def test_plugin_availability_disabled(self, mock_config): """Test plugin is not available when disabled.""" from fastapi_app.plugins.local_sync.plugin import LocalSyncPlugin # Mock config to return disabled def config_side_effect(key, env_var, default=None, value_type="string"): if key == "plugin.local-sync.enabled": return False return None mock_config.side_effect = config_side_effect self.assertFalse(LocalSyncPlugin.is_available()) @patch('fastapi_app.lib.plugins.plugin_tools.get_plugin_config') def test_plugin_availability_no_repo_path(self, mock_config): """Test plugin is not available when repo path not configured.""" from fastapi_app.plugins.local_sync.plugin import LocalSyncPlugin # Mock config to return enabled but no repo path def config_side_effect(key, env_var, default=None, value_type="string"): if key == "plugin.local-sync.enabled": return True if key == "plugin.local-sync.repo.path": return None return None mock_config.side_effect = config_side_effect self.assertFalse(LocalSyncPlugin.is_available()) @patch('fastapi_app.lib.plugins.plugin_tools.get_plugin_config') def test_plugin_availability_enabled(self, mock_config): """Test plugin is available when enabled and repo path configured.""" from fastapi_app.plugins.local_sync.plugin import LocalSyncPlugin # Mock config to return enabled and repo path def config_side_effect(key, env_var, default=None, value_type="string"): if key == "plugin.local-sync.enabled": return True if key == "plugin.local-sync.repo.path": return "/some/path" return None mock_config.side_effect = config_side_effect self.assertTrue(LocalSyncPlugin.is_available()) def test_update_filesystem_with_backup(self): """Test filesystem update creates backup.""" from fastapi_app.plugins.local_sync.plugin import LocalSyncPlugin plugin = LocalSyncPlugin() test_path = Path(self.temp_dir) test_file = test_path / "test.tei.xml" test_file.write_text("old content") plugin._update_filesystem(test_file, b"new content", backup_enabled=True) # Check new content self.assertEqual(test_file.read_text(), "new content") # Check backup exists backups = list(test_path.glob("test.*.backup")) self.assertEqual(len(backups), 1) self.assertEqual(backups[0].read_text(), "old content") def test_update_filesystem_without_backup(self): """Test filesystem update without backup.""" from fastapi_app.plugins.local_sync.plugin import LocalSyncPlugin plugin = LocalSyncPlugin() test_path = Path(self.temp_dir) test_file = test_path / "test.tei.xml" test_file.write_text("old content") plugin._update_filesystem(test_file, b"new content", backup_enabled=False) # Check new content self.assertEqual(test_file.read_text(), "new content") # Check no backup exists backups = list(test_path.glob("test.*.backup")) self.assertEqual(len(backups), 0) def test_create_new_version_preserves_processing_instructions(self): """Test that _create_new_version preserves processing instructions from filesystem.""" from fastapi_app.plugins.local_sync.plugin import LocalSyncPlugin from unittest.mock import Mock import hashlib # TEI content with processing instruction tei_content = b""" Test Edition

Test content

""" plugin = LocalSyncPlugin() # Mock dependencies mock_file_repo = Mock() mock_file_storage = Mock() mock_doc = Mock() mock_doc.doc_id = "test-doc" mock_doc.variant = "test" mock_doc.doc_collections = ["test-collection"] mock_user = {"username": "testuser", "fullname": "Test User"} # Track saved content saved_content = None # Mock file_storage.save_file to capture content def mock_save_file(content, file_type, increment_ref=False): nonlocal saved_content saved_content = content content_hash = hashlib.sha256(content).hexdigest() return content_hash, f"/path/to/{content_hash}" mock_file_storage.save_file = Mock(side_effect=mock_save_file) # Mock file_repo.get_latest_tei_version mock_file_repo.get_latest_tei_version.return_value = None # Mock file_repo.insert_file to return the created file def mock_insert_file(file_create): mock_file = Mock() mock_file.stable_id = "test-stable-id" return mock_file mock_file_repo.insert_file = Mock(side_effect=mock_insert_file) # Call _create_new_version result = plugin._create_new_version( mock_file_repo, mock_file_storage, mock_doc, tei_content, mock_user ) # Get the content that was saved saved_content_str = saved_content.decode('utf-8') # Verify processing instruction is preserved self.assertIn('', saved_content_str) if __name__ == '__main__': unittest.main()