From 989902d86e3e40a333427870f4bfd70a6ec70db3 Mon Sep 17 00:00:00 2001 From: HERITAGE-XION Date: Tue, 6 Jan 2026 21:49:46 +0100 Subject: [PATCH] metadata-scrubber-tool: added unit test for image handler --- scripts/generate_png_tests.py | 29 ++++++ src/services/image_handler.py | 3 +- src/services/metadata_factory.py | 8 +- tests/__init__.py | 0 tests/e2e/__init__.py | 0 tests/integration/__init__.py | 0 tests/unit/__init__.py | 0 tests/unit/test_image_handler.py | 163 +++++++++++++++++++++++++++++++ 8 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 scripts/generate_png_tests.py create mode 100644 tests/__init__.py create mode 100644 tests/e2e/__init__.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/test_image_handler.py diff --git a/scripts/generate_png_tests.py b/scripts/generate_png_tests.py new file mode 100644 index 00000000..8d40e3ef --- /dev/null +++ b/scripts/generate_png_tests.py @@ -0,0 +1,29 @@ +"""Generate PNG test images with metadata.""" + +import random +from pathlib import Path + +from PIL import Image, PngImagePlugin + +dest_dir = Path("tests/assets/test_images") + +# Generate 45 PNG images with various metadata +for i in range(1, 46): + # Create a simple colored image + r, g, b = random.randint(50, 200), random.randint(50, 200), random.randint(50, 200) + img = Image.new("RGB", (100, 100), color=(r, g, b)) + + # Add PNG text metadata + pnginfo = PngImagePlugin.PngInfo() + pnginfo.add_text("Author", f"Test Author {i}") + pnginfo.add_text("Description", f"Test image number {i} for metadata scrubbing") + pnginfo.add_text("Software", "Python PIL Test Generator") + pnginfo.add_text("Copyright", f"Copyright 2024 Test {i}") + pnginfo.add_text("Creation Time", f"2024-01-{i % 28 + 1:02d}") + + # Save with metadata + filename = dest_dir / f"generated_test_{i:02d}.png" + img.save(filename, pnginfo=pnginfo) + +print("Generated 45 PNG images with metadata") +print(f"Total PNG count: {len(list(dest_dir.glob('*.png')))}") diff --git a/src/services/image_handler.py b/src/services/image_handler.py index 0259d501..ede104a9 100644 --- a/src/services/image_handler.py +++ b/src/services/image_handler.py @@ -131,7 +131,7 @@ class ImageHandler(MetadataHandler): img, self.text_keys_to_delete ) - def save(self, output_path: Optional[str] = None) -> None: + def save(self, output_path: Optional[str | Path] = None) -> None: """ Writes the changes to a copy of the original file. @@ -156,6 +156,7 @@ class ImageHandler(MetadataHandler): with Image.open(destination_file_path) as img: exif_bytes = piexif.dump(self.processed_metadata) img.save(destination_file_path, exif=exif_bytes) + elif actual_format == "png": # PNG: Open original, save fresh copy without metadata with Image.open(Path(self.filepath)) as img: diff --git a/src/services/metadata_factory.py b/src/services/metadata_factory.py index 27089b17..5ca789b7 100644 --- a/src/services/metadata_factory.py +++ b/src/services/metadata_factory.py @@ -40,6 +40,7 @@ class MetadataFactory: UnsupportedFormatError: If no handler is defined for the file type. ValueError: If the path is not a valid file. """ + supported_extensions = ".jpg, .jpeg, .png" ext = Path(filepath).suffix.lower() if Path(filepath).is_file(): if ext in [".jpg", ".jpeg", ".png"]: @@ -53,8 +54,11 @@ class MetadataFactory: # elif ext == ".pptx": # return PowerPointHandler(filepath) else: - raise UnsupportedFormatError(f"No handler defined for {ext} files.") + print("i ran") + raise UnsupportedFormatError( + f"No handler defined for {ext} files. we curently only support {supported_extensions} files." + ) else: raise ValueError( - f"{filepath} is not a file. if you want to process a directory, use the --recursive flag." + f"{filepath} is not a file. if you want to process a directory, use the --recursive or -r flag." ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/e2e/__init__.py b/tests/e2e/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/test_image_handler.py b/tests/unit/test_image_handler.py new file mode 100644 index 00000000..862685bc --- /dev/null +++ b/tests/unit/test_image_handler.py @@ -0,0 +1,163 @@ +import shutil +from pathlib import Path + +import pytest + +from src.services.image_handler import ImageHandler +from src.utils.exceptions import MetadataNotFoundError, UnsupportedFormatError + +# Test file paths +JPG_TEST_FILE = r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images\DSCN0012.jpg" +PNG_TEST_FILE = r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images\generated_test_45.png" + + +# ============== success Case Tests ============== + + +@pytest.mark.parametrize( + "x", + [JPG_TEST_FILE, PNG_TEST_FILE], +) +def test_read_image_metadata(x): + """ + test for reading image metadata + """ + # checks if file exists + assert Path(x).exists() + processor = ImageHandler(x) + metadata = processor.read() + + # checks if metadata is not empty + assert processor.metadata == metadata + + # checks if tags_to_delete or text_keys_to_delete is not empty + assert ( + processor.tags_to_delete is not None + or processor.text_keys_to_delete is not None + ) + + # checks if metadata is a dictionary + assert isinstance(metadata, dict) + + +@pytest.mark.parametrize( + "x", + [JPG_TEST_FILE, PNG_TEST_FILE], +) +def test_wipe_image_metadata(x): + """ + test for wiping image metadata + """ + # checks if file exists + assert Path(x).exists() + processor = ImageHandler(x) + metadata = processor.read() + processor.wipe() + + # checks if processed_metadata is not equal to metadata + assert processor.processed_metadata != metadata + + +@pytest.mark.parametrize( + "x", + [JPG_TEST_FILE, PNG_TEST_FILE], +) +def test_save_processed_image_metadata(x): + """ + test for saving image processed metadata to a copy of the file + """ + # creates output directory + output_dir = Path("./tests/assets/output") + output_dir.mkdir(parents=True, exist_ok=True) + + processor = ImageHandler(x) + metadata = processor.read() + processor.wipe() + + # checks if processed_metadata is not equal to metadata + assert processor.processed_metadata != metadata + + # Pass full file path + output_file = output_dir / Path(x).name + processor.save(str(output_file)) + + # checks if output file exists and then deletes it + assert output_file.exists() + shutil.rmtree(output_dir) + + +@pytest.mark.parametrize( + "x", + [JPG_TEST_FILE, PNG_TEST_FILE], +) +def test_output_file_has_less_metadata(x): + """ + Test that the output file has metadata stripped + """ + output_dir = Path("./tests/assets/output") + output_dir.mkdir(parents=True, exist_ok=True) + + # Process original file + processor = ImageHandler(x) + original_metadata = processor.read() + original_count = len(original_metadata) + processor.wipe() + + # Save processed file + output_file = output_dir / Path(x).name + processor.save(str(output_file)) + + # Read output file and verify metadata is reduced or gone + try: + output_processor = ImageHandler(str(output_file)) + output_metadata = output_processor.read() + # Output should have fewer metadata entries + assert len(output_metadata) < original_count + except MetadataNotFoundError: + # If no metadata found, that's expected for fully stripped files + pass + # clean up + shutil.rmtree(output_dir) + + +def test_format_detection_works(): + """ + Test that format detection uses Pillow, not file extension + """ + handler = ImageHandler(JPG_TEST_FILE) + detected = handler._detect_format() + assert detected == "jpeg" + + handler_png = ImageHandler(PNG_TEST_FILE) + detected_png = handler_png._detect_format() + assert detected_png == "png" + + +# ============== Error Case Tests ============== + + +def test_unsupported_format_raises_error(tmp_path): + """ + Test that unsupported file formats raise an error (PIL or custom) + """ + from PIL import UnidentifiedImageError + + # Create a fake text file + fake_file = tmp_path / "test.txt" + fake_file.write_text("not an image") + + handler = ImageHandler(str(fake_file)) + # Either our UnsupportedFormatError or PIL's UnidentifiedImageError is acceptable + with pytest.raises((UnsupportedFormatError, UnidentifiedImageError)): + handler.read() + + +def test_save_without_output_path_raises_error(): + """ + Test that save() raises ValueError when output_path is None + """ + handler = ImageHandler(JPG_TEST_FILE) + handler.read() + handler.wipe() + with pytest.raises(ValueError): + handler.save(None)