""" Comprehensive tests for project_serializer module """ import pytest import os import json import zipfile import tempfile import shutil from pathlib import Path from unittest.mock import Mock, patch, MagicMock from pyPhotoAlbum.project_serializer import ( save_to_zip, load_from_zip, get_project_info, _normalize_asset_paths, _import_external_images, SERIALIZATION_VERSION, ) from pyPhotoAlbum.project import Project from pyPhotoAlbum.models import ImageData class TestSaveToZip: """Tests for save_to_zip function""" def test_save_to_zip_basic(self, tmp_path): """Test basic project saving to zip""" project_folder = tmp_path / "project" project_folder.mkdir() assets_folder = project_folder / "assets" assets_folder.mkdir() project = Project(name="TestProject", folder_path=str(project_folder)) zip_path = str(tmp_path / "test_project.ppz") success, error = save_to_zip(project, zip_path) assert success is True assert error is None assert os.path.exists(zip_path) def test_save_to_zip_adds_extension(self, tmp_path): """Test that .ppz extension is added if missing""" project_folder = tmp_path / "project" project_folder.mkdir() assets_folder = project_folder / "assets" assets_folder.mkdir() project = Project(name="TestProject", folder_path=str(project_folder)) zip_path = str(tmp_path / "test_project") # No extension success, error = save_to_zip(project, zip_path) assert success is True assert os.path.exists(zip_path + ".ppz") def test_save_to_zip_includes_project_json(self, tmp_path): """Test that saved zip contains project.json""" project_folder = tmp_path / "project" project_folder.mkdir() assets_folder = project_folder / "assets" assets_folder.mkdir() project = Project(name="TestProject", folder_path=str(project_folder)) zip_path = str(tmp_path / "test_project.ppz") save_to_zip(project, zip_path) with zipfile.ZipFile(zip_path, "r") as zipf: assert "project.json" in zipf.namelist() project_data = json.loads(zipf.read("project.json")) assert project_data["name"] == "TestProject" assert "data_version" in project_data def test_save_to_zip_includes_assets(self, tmp_path): """Test that saved zip includes asset files""" project_folder = tmp_path / "project" project_folder.mkdir() assets_folder = project_folder / "assets" assets_folder.mkdir() # Create a dummy asset file asset_file = assets_folder / "image.jpg" asset_file.write_bytes(b"fake image data") project = Project(name="TestProject", folder_path=str(project_folder)) zip_path = str(tmp_path / "test_project.ppz") save_to_zip(project, zip_path) with zipfile.ZipFile(zip_path, "r") as zipf: assert "assets/image.jpg" in zipf.namelist() def test_save_to_zip_handles_error(self, tmp_path): """Test error handling during save""" project_folder = tmp_path / "project" project_folder.mkdir() assets_folder = project_folder / "assets" assets_folder.mkdir() project = Project(name="TestProject", folder_path=str(project_folder)) # Try to save to an invalid path zip_path = "/nonexistent/directory/test.ppz" success, error = save_to_zip(project, zip_path) assert success is False assert error is not None assert "Error saving" in error class TestLoadFromZip: """Tests for load_from_zip function""" def test_load_from_zip_basic(self, tmp_path): """Test basic project loading from zip""" # First create a valid project zip project_folder = tmp_path / "project" project_folder.mkdir() assets_folder = project_folder / "assets" assets_folder.mkdir() project = Project(name="LoadTest", folder_path=str(project_folder)) zip_path = str(tmp_path / "test_project.ppz") save_to_zip(project, zip_path) # Now load it extract_to = str(tmp_path / "extracted") loaded_project = load_from_zip(zip_path, extract_to) assert loaded_project.name == "LoadTest" assert loaded_project.folder_path == extract_to def test_load_from_zip_creates_temp_dir(self, tmp_path): """Test that loading creates a temp directory when none specified""" project_folder = tmp_path / "project" project_folder.mkdir() assets_folder = project_folder / "assets" assets_folder.mkdir() project = Project(name="TempTest", folder_path=str(project_folder)) zip_path = str(tmp_path / "test_project.ppz") save_to_zip(project, zip_path) # Load without specifying extraction directory loaded_project = load_from_zip(zip_path) assert loaded_project.name == "TempTest" assert loaded_project.folder_path is not None assert os.path.exists(loaded_project.folder_path) # Should have a _temp_dir attribute assert hasattr(loaded_project, "_temp_dir") def test_load_from_zip_file_not_found(self, tmp_path): """Test loading from nonexistent file""" with pytest.raises(FileNotFoundError): load_from_zip(str(tmp_path / "nonexistent.ppz")) def test_load_from_zip_invalid_zip(self, tmp_path): """Test loading from invalid zip file""" invalid_file = tmp_path / "invalid.ppz" invalid_file.write_text("not a zip file") with pytest.raises(Exception): load_from_zip(str(invalid_file)) def test_load_from_zip_missing_project_json(self, tmp_path): """Test loading from zip without project.json""" zip_path = tmp_path / "no_project.ppz" # Create zip without project.json with zipfile.ZipFile(str(zip_path), "w") as zipf: zipf.writestr("other_file.txt", "some content") with pytest.raises(ValueError) as exc_info: load_from_zip(str(zip_path)) assert "project.json not found" in str(exc_info.value) class TestGetProjectInfo: """Tests for get_project_info function""" def test_get_project_info_basic(self, tmp_path): """Test getting project info from zip""" project_folder = tmp_path / "project" project_folder.mkdir() assets_folder = project_folder / "assets" assets_folder.mkdir() project = Project(name="InfoTest", folder_path=str(project_folder)) zip_path = str(tmp_path / "test_project.ppz") save_to_zip(project, zip_path) info = get_project_info(zip_path) assert info is not None assert info["name"] == "InfoTest" assert "version" in info assert "page_count" in info assert "page_size_mm" in info assert "working_dpi" in info def test_get_project_info_invalid_file(self, tmp_path): """Test getting info from invalid file""" invalid_file = tmp_path / "invalid.ppz" invalid_file.write_text("not a zip") info = get_project_info(str(invalid_file)) assert info is None def test_get_project_info_nonexistent_file(self, tmp_path): """Test getting info from nonexistent file""" info = get_project_info(str(tmp_path / "nonexistent.ppz")) assert info is None class TestNormalizeAssetPaths: """Tests for _normalize_asset_paths function""" def test_normalize_relative_path_unchanged(self, tmp_path): """Test that simple relative paths are unchanged""" project_folder = tmp_path / "project" project_folder.mkdir() assets_folder = project_folder / "assets" assets_folder.mkdir() project = Project(name="Test", folder_path=str(project_folder)) # Add a page with an image that has a simple relative path from pyPhotoAlbum.page_layout import PageLayout page_mock = Mock() layout = PageLayout(width=210, height=297) img = ImageData(image_path="assets/image.jpg") layout.add_element(img) page_mock.layout = layout project.pages = [page_mock] _normalize_asset_paths(project, str(project_folder)) # Path should be unchanged assert img.image_path == "assets/image.jpg" def test_normalize_absolute_path(self, tmp_path): """Test that absolute paths are normalized""" project_folder = tmp_path / "project" project_folder.mkdir() assets_folder = project_folder / "assets" assets_folder.mkdir() project = Project(name="Test", folder_path=str(project_folder)) from pyPhotoAlbum.page_layout import PageLayout page_mock = Mock() layout = PageLayout(width=210, height=297) # Use a path that contains /assets/ pattern abs_path = str(project_folder / "assets" / "image.jpg") img = ImageData(image_path=abs_path) layout.add_element(img) page_mock.layout = layout project.pages = [page_mock] _normalize_asset_paths(project, str(project_folder)) # Path should be normalized to relative assert img.image_path == "assets/image.jpg" def test_normalize_legacy_path(self, tmp_path): """Test normalizing legacy project path format""" project_folder = tmp_path / "project" project_folder.mkdir() assets_folder = project_folder / "assets" assets_folder.mkdir() project = Project(name="Test", folder_path=str(project_folder)) from pyPhotoAlbum.page_layout import PageLayout page_mock = Mock() layout = PageLayout(width=210, height=297) # Legacy path format img = ImageData(image_path="./projects/old_project/assets/image.jpg") layout.add_element(img) page_mock.layout = layout project.pages = [page_mock] _normalize_asset_paths(project, str(project_folder)) # Should extract just the assets/filename part assert img.image_path == "assets/image.jpg" class TestImportExternalImages: """Tests for _import_external_images function""" def test_import_external_images_no_external(self, tmp_path): """Test with no external images""" project_folder = tmp_path / "project" project_folder.mkdir() assets_folder = project_folder / "assets" assets_folder.mkdir() project = Project(name="Test", folder_path=str(project_folder)) from pyPhotoAlbum.page_layout import PageLayout page_mock = Mock() layout = PageLayout(width=210, height=297) img = ImageData(image_path="assets/existing.jpg") layout.add_element(img) page_mock.layout = layout project.pages = [page_mock] # Should not raise and not change path _import_external_images(project) assert img.image_path == "assets/existing.jpg" class TestRoundTrip: """Test save and load roundtrip""" def test_roundtrip_basic(self, tmp_path): """Test saving and loading a project""" project_folder = tmp_path / "project" project_folder.mkdir() assets_folder = project_folder / "assets" assets_folder.mkdir() original = Project(name="RoundTrip", folder_path=str(project_folder)) original.working_dpi = 150 zip_path = str(tmp_path / "roundtrip.ppz") success, _ = save_to_zip(original, zip_path) assert success extract_to = str(tmp_path / "extracted") loaded = load_from_zip(zip_path, extract_to) assert loaded.name == original.name assert loaded.working_dpi == original.working_dpi def test_roundtrip_with_pages(self, tmp_path): """Test roundtrip with pages""" project_folder = tmp_path / "project" project_folder.mkdir() assets_folder = project_folder / "assets" assets_folder.mkdir() original = Project(name="WithPages", folder_path=str(project_folder)) # Project starts with 1 page, add more using create_page from pyPhotoAlbum.project import Page from pyPhotoAlbum.page_layout import PageLayout page2 = Page(PageLayout(width=210, height=297)) page3 = Page(PageLayout(width=210, height=297)) original.add_page(page2) original.add_page(page3) zip_path = str(tmp_path / "pages.ppz") save_to_zip(original, zip_path) extract_to = str(tmp_path / "extracted") loaded = load_from_zip(zip_path, extract_to) # Pages are preserved (Project might not start with a default page) assert len(loaded.pages) >= 2 def test_roundtrip_with_elements(self, tmp_path, temp_image_file): """Test roundtrip with elements on page""" project_folder = tmp_path / "project" project_folder.mkdir() assets_folder = project_folder / "assets" assets_folder.mkdir() # Copy temp image to assets shutil.copy(temp_image_file, assets_folder / "test.jpg") original = Project(name="WithElements", folder_path=str(project_folder)) # Add element to first page (project starts with at least 1 page) img = ImageData(image_path="assets/test.jpg", x=50, y=50, width=100, height=100) # Check if there's a default page, add one if needed if not original.pages: from pyPhotoAlbum.project import Page from pyPhotoAlbum.page_layout import PageLayout original.add_page(Page(PageLayout(width=210, height=297))) original.pages[0].layout.add_element(img) zip_path = str(tmp_path / "elements.ppz") save_to_zip(original, zip_path) extract_to = str(tmp_path / "extracted") loaded = load_from_zip(zip_path, extract_to) assert len(loaded.pages) >= 1 assert len(loaded.pages[0].layout.elements) >= 1 loaded_elem = loaded.pages[0].layout.elements[0] assert loaded_elem.position == (50.0, 50.0) assert loaded_elem.size == (100.0, 100.0) class TestVersionCompatibility: """Tests for version handling""" def test_version_included_in_save(self, tmp_path): """Test that version is included when saving""" project_folder = tmp_path / "project" project_folder.mkdir() assets_folder = project_folder / "assets" assets_folder.mkdir() project = Project(name="Version", folder_path=str(project_folder)) zip_path = str(tmp_path / "version.ppz") save_to_zip(project, zip_path) with zipfile.ZipFile(zip_path, "r") as zipf: data = json.loads(zipf.read("project.json")) # Should have both legacy and new version fields assert "serialization_version" in data assert "data_version" in data