""" Unit tests for project serialization (save/load to ZIP) """ import pytest import os import json import zipfile import tempfile import shutil from pathlib import Path from pyPhotoAlbum.project import Project, Page from pyPhotoAlbum.page_layout import PageLayout from pyPhotoAlbum.models import ImageData, TextBoxData from pyPhotoAlbum.project_serializer import save_to_zip, load_from_zip, get_project_info @pytest.fixture def temp_dir(): """Create a temporary directory for testing""" temp_path = tempfile.mkdtemp() yield temp_path # Cleanup if os.path.exists(temp_path): shutil.rmtree(temp_path) @pytest.fixture def sample_project(temp_dir): """Create a sample project for testing""" project = Project(name="Test Project", folder_path=os.path.join(temp_dir, "test_project")) project.page_size_mm = (210, 297) project.working_dpi = 300 project.export_dpi = 300 return project @pytest.fixture def sample_image(temp_dir): """Create a sample image file for testing""" from PIL import Image # Create a simple test image img = Image.new('RGB', (100, 100), color='red') image_path = os.path.join(temp_dir, "test_image.jpg") img.save(image_path) return image_path class TestBasicSerialization: """Tests for basic save/load functionality""" def test_save_empty_project(self, sample_project, temp_dir): """Test saving an empty project to ZIP""" zip_path = os.path.join(temp_dir, "empty_project.ppz") success, error = save_to_zip(sample_project, zip_path) assert success is True assert error is None assert os.path.exists(zip_path) assert zip_path.endswith('.ppz') def test_save_adds_ppz_extension(self, sample_project, temp_dir): """Test that .ppz extension is added automatically""" zip_path = os.path.join(temp_dir, "project") success, error = save_to_zip(sample_project, zip_path) assert success is True expected_path = zip_path + '.ppz' assert os.path.exists(expected_path) def test_load_empty_project(self, sample_project, temp_dir): """Test loading an empty project from ZIP""" zip_path = os.path.join(temp_dir, "empty_project.ppz") save_to_zip(sample_project, zip_path) loaded_project, error = load_from_zip(zip_path) assert loaded_project is not None assert error is None assert loaded_project.name == "Test Project" assert loaded_project.page_size_mm == (210, 297) assert loaded_project.working_dpi == 300 assert len(loaded_project.pages) == 0 def test_load_nonexistent_file(self, temp_dir): """Test loading from a non-existent file""" zip_path = os.path.join(temp_dir, "nonexistent.ppz") loaded_project, error = load_from_zip(zip_path) assert loaded_project is None assert error is not None assert "not found" in error.lower() def test_save_project_with_pages(self, sample_project, temp_dir): """Test saving a project with multiple pages""" # Add pages for i in range(3): layout = PageLayout() page = Page(layout=layout, page_number=i+1) sample_project.add_page(page) zip_path = os.path.join(temp_dir, "project_with_pages.ppz") success, error = save_to_zip(sample_project, zip_path) assert success is True assert os.path.exists(zip_path) def test_load_project_with_pages(self, sample_project, temp_dir): """Test loading a project with multiple pages""" # Add pages for i in range(3): layout = PageLayout() page = Page(layout=layout, page_number=i+1) sample_project.add_page(page) # Save and load zip_path = os.path.join(temp_dir, "project_with_pages.ppz") save_to_zip(sample_project, zip_path) loaded_project, error = load_from_zip(zip_path) assert loaded_project is not None assert len(loaded_project.pages) == 3 assert loaded_project.pages[0].page_number == 1 assert loaded_project.pages[2].page_number == 3 class TestZipStructure: """Tests for ZIP file structure and content""" def test_zip_contains_project_json(self, sample_project, temp_dir): """Test that ZIP contains project.json""" zip_path = os.path.join(temp_dir, "test.ppz") save_to_zip(sample_project, zip_path) with zipfile.ZipFile(zip_path, 'r') as zipf: assert 'project.json' in zipf.namelist() def test_project_json_is_valid(self, sample_project, temp_dir): """Test that project.json contains valid JSON""" zip_path = os.path.join(temp_dir, "test.ppz") save_to_zip(sample_project, zip_path) with zipfile.ZipFile(zip_path, 'r') as zipf: project_json = zipf.read('project.json').decode('utf-8') data = json.loads(project_json) assert 'name' in data assert 'serialization_version' in data assert data['name'] == "Test Project" def test_version_in_serialized_data(self, sample_project, temp_dir): """Test that version information is included""" zip_path = os.path.join(temp_dir, "test.ppz") save_to_zip(sample_project, zip_path) with zipfile.ZipFile(zip_path, 'r') as zipf: project_json = zipf.read('project.json').decode('utf-8') data = json.loads(project_json) assert 'serialization_version' in data assert data['serialization_version'] == "1.0" class TestAssetManagement: """Tests for asset bundling and management""" def test_save_project_with_image(self, sample_project, sample_image, temp_dir): """Test saving a project with an image""" # Import image to project imported_path = sample_project.asset_manager.import_asset(sample_image) # Create page with image layout = PageLayout() img_data = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100) layout.add_element(img_data) page = Page(layout=layout, page_number=1) sample_project.add_page(page) # Save zip_path = os.path.join(temp_dir, "project_with_image.ppz") success, error = save_to_zip(sample_project, zip_path) assert success is True assert os.path.exists(zip_path) def test_assets_folder_in_zip(self, sample_project, sample_image, temp_dir): """Test that assets folder is included in ZIP""" # Import image imported_path = sample_project.asset_manager.import_asset(sample_image) # Create page with image layout = PageLayout() img_data = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100) layout.add_element(img_data) page = Page(layout=layout, page_number=1) sample_project.add_page(page) # Save zip_path = os.path.join(temp_dir, "project_with_image.ppz") save_to_zip(sample_project, zip_path) # Check ZIP contents with zipfile.ZipFile(zip_path, 'r') as zipf: names = zipf.namelist() # Should contain assets folder asset_files = [n for n in names if n.startswith('assets/')] assert len(asset_files) > 0 def test_load_project_with_image(self, sample_project, sample_image, temp_dir): """Test loading a project with images""" # Import image imported_path = sample_project.asset_manager.import_asset(sample_image) # Create page with image layout = PageLayout() img_data = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100) layout.add_element(img_data) page = Page(layout=layout, page_number=1) sample_project.add_page(page) # Save and load zip_path = os.path.join(temp_dir, "project_with_image.ppz") save_to_zip(sample_project, zip_path) loaded_project, error = load_from_zip(zip_path) assert loaded_project is not None assert len(loaded_project.pages) == 1 assert len(loaded_project.pages[0].layout.elements) == 1 # Verify image element img_element = loaded_project.pages[0].layout.elements[0] assert isinstance(img_element, ImageData) assert img_element.image_path != "" def test_asset_reference_counts_preserved(self, sample_project, sample_image, temp_dir): """Test that asset reference counts are preserved""" # Import image imported_path = sample_project.asset_manager.import_asset(sample_image) # Use image twice layout1 = PageLayout() img1 = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100) layout1.add_element(img1) page1 = Page(layout=layout1, page_number=1) sample_project.add_page(page1) layout2 = PageLayout() img2 = ImageData(image_path=imported_path, x=20, y=20, width=100, height=100) layout2.add_element(img2) page2 = Page(layout=layout2, page_number=2) sample_project.add_page(page2) # Get relative path for reference count check rel_path = os.path.relpath(imported_path, sample_project.folder_path) original_ref_count = sample_project.asset_manager.get_reference_count(rel_path) # Save and load zip_path = os.path.join(temp_dir, "project_refs.ppz") save_to_zip(sample_project, zip_path) loaded_project, error = load_from_zip(zip_path) assert loaded_project is not None # Reference counts should be preserved # Note: The actual reference counting behavior depends on deserialize implementation class TestPortability: """Tests for project portability across different locations""" def test_load_to_different_directory(self, sample_project, sample_image, temp_dir): """Test loading project to a different directory""" # Import image and create page imported_path = sample_project.asset_manager.import_asset(sample_image) layout = PageLayout() img_data = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100) layout.add_element(img_data) page = Page(layout=layout, page_number=1) sample_project.add_page(page) # Save zip_path = os.path.join(temp_dir, "portable_project.ppz") save_to_zip(sample_project, zip_path) # Load to a different location new_location = os.path.join(temp_dir, "different_location") loaded_project, error = load_from_zip(zip_path, extract_to=new_location) assert loaded_project is not None assert loaded_project.folder_path == new_location assert os.path.exists(new_location) # Verify assets were extracted assets_folder = os.path.join(new_location, "assets") assert os.path.exists(assets_folder) def test_relative_paths_work_after_move(self, sample_project, sample_image, temp_dir): """Test that relative paths still work after loading to different location""" # Import image imported_path = sample_project.asset_manager.import_asset(sample_image) layout = PageLayout() img_data = ImageData(image_path=imported_path, x=10, y=10, width=100, height=100) layout.add_element(img_data) page = Page(layout=layout, page_number=1) sample_project.add_page(page) # Save zip_path = os.path.join(temp_dir, "portable_project.ppz") save_to_zip(sample_project, zip_path) # Load to different location new_location = os.path.join(temp_dir, "new_location") loaded_project, error = load_from_zip(zip_path, extract_to=new_location) # Verify image path is accessible from new location img_element = loaded_project.pages[0].layout.elements[0] image_path = img_element.image_path # Image path should exist # Note: May be absolute or relative depending on implementation if not os.path.isabs(image_path): full_path = os.path.join(loaded_project.folder_path, image_path) assert os.path.exists(full_path) else: assert os.path.exists(image_path) class TestProjectInfo: """Tests for get_project_info utility function""" def test_get_project_info(self, sample_project, temp_dir): """Test getting project info without loading""" # Add some pages for i in range(5): layout = PageLayout() page = Page(layout=layout, page_number=i+1) sample_project.add_page(page) # Save zip_path = os.path.join(temp_dir, "info_test.ppz") save_to_zip(sample_project, zip_path) # Get info info = get_project_info(zip_path) assert info is not None assert info['name'] == "Test Project" assert info['page_count'] == 5 assert info['version'] == "1.0" assert info['working_dpi'] == 300 def test_get_info_invalid_zip(self, temp_dir): """Test getting info from invalid ZIP""" zip_path = os.path.join(temp_dir, "invalid.ppz") info = get_project_info(zip_path) assert info is None class TestEdgeCases: """Tests for edge cases and error handling""" def test_save_to_invalid_path(self, sample_project): """Test saving to an invalid path""" invalid_path = "/nonexistent/directory/project.ppz" success, error = save_to_zip(sample_project, invalid_path) assert success is False assert error is not None def test_load_corrupted_zip(self, temp_dir): """Test loading a corrupted ZIP file""" # Create a fake corrupted file corrupted_path = os.path.join(temp_dir, "corrupted.ppz") with open(corrupted_path, 'w') as f: f.write("This is not a ZIP file") loaded_project, error = load_from_zip(corrupted_path) assert loaded_project is None assert error is not None def test_load_zip_without_project_json(self, temp_dir): """Test loading a ZIP without project.json""" zip_path = os.path.join(temp_dir, "no_json.ppz") # Create ZIP without project.json with zipfile.ZipFile(zip_path, 'w') as zipf: zipf.writestr('dummy.txt', 'dummy content') loaded_project, error = load_from_zip(zip_path) assert loaded_project is None assert error is not None assert "project.json not found" in error def test_project_with_text_elements(self, sample_project, temp_dir): """Test saving/loading project with text elements""" # Create page with text layout = PageLayout() text = TextBoxData( text_content="Hello World", x=10, y=10, width=200, height=50 ) layout.add_element(text) page = Page(layout=layout, page_number=1) sample_project.add_page(page) # Save and load zip_path = os.path.join(temp_dir, "with_text.ppz") save_to_zip(sample_project, zip_path) loaded_project, error = load_from_zip(zip_path) assert loaded_project is not None assert len(loaded_project.pages) == 1 text_element = loaded_project.pages[0].layout.elements[0] assert isinstance(text_element, TextBoxData) assert text_element.text_content == "Hello World"