This commit is contained in:
Danilo Reyes
2025-11-02 01:47:25 -06:00
parent 48020b6f42
commit 010df31455
45 changed files with 8045 additions and 720 deletions

View File

@@ -0,0 +1,2 @@
"""Image tests package."""

View File

@@ -0,0 +1,79 @@
"""Tests for image processing and thumbnail generation."""
import io
from uuid import uuid4
import pytest
from PIL import Image as PILImage
from app.images.processing import generate_thumbnails
class TestThumbnailGeneration:
"""Tests for thumbnail generation."""
def test_generate_thumbnails_creates_all_sizes(self):
"""Test that thumbnails are generated for all quality levels."""
# Create a test image
image_id = uuid4()
image = PILImage.new("RGB", (2000, 1500), color="red")
buffer = io.BytesIO()
image.save(buffer, format="JPEG")
contents = buffer.getvalue()
# Mock storage client to avoid actual uploads
from unittest.mock import MagicMock, patch
with patch("app.images.processing.get_storage_client") as mock_storage:
mock_storage.return_value.put_object = MagicMock()
# Generate thumbnails
thumbnail_paths = generate_thumbnails(image_id, "test/path.jpg", contents)
# Verify all sizes created
assert "low" in thumbnail_paths
assert "medium" in thumbnail_paths
assert "high" in thumbnail_paths
# Verify storage was called
assert mock_storage.return_value.put_object.call_count >= 2
def test_skip_thumbnail_for_small_images(self):
"""Test that thumbnails are skipped if image is smaller than target size."""
# Create a small test image (smaller than low quality threshold)
image_id = uuid4()
image = PILImage.new("RGB", (500, 375), color="blue")
buffer = io.BytesIO()
image.save(buffer, format="JPEG")
contents = buffer.getvalue()
from unittest.mock import MagicMock, patch
with patch("app.images.processing.get_storage_client") as mock_storage:
mock_storage.return_value.put_object = MagicMock()
# Generate thumbnails
thumbnail_paths = generate_thumbnails(image_id, "test/small.jpg", contents)
# Should use original path for all sizes
assert thumbnail_paths["low"] == "test/small.jpg"
def test_handles_transparent_images(self):
"""Test conversion of transparent images to RGB."""
# Create RGBA image
image_id = uuid4()
image = PILImage.new("RGBA", (2000, 1500), color=(255, 0, 0, 128))
buffer = io.BytesIO()
image.save(buffer, format="PNG")
contents = buffer.getvalue()
from unittest.mock import MagicMock, patch
with patch("app.images.processing.get_storage_client") as mock_storage:
mock_storage.return_value.put_object = MagicMock()
# Should not raise exception
thumbnail_paths = generate_thumbnails(image_id, "test/transparent.png", contents)
assert len(thumbnail_paths) > 0

View File

@@ -0,0 +1,82 @@
"""Tests for file validation."""
import io
from unittest.mock import AsyncMock, Mock
import pytest
from fastapi import HTTPException, UploadFile
from app.images.validation import sanitize_filename, validate_image_file
class TestSanitizeFilename:
"""Tests for filename sanitization."""
def test_sanitize_normal_filename(self):
"""Test sanitizing normal filename."""
assert sanitize_filename("image.jpg") == "image.jpg"
assert sanitize_filename("my_photo-2025.png") == "my_photo-2025.png"
def test_sanitize_path_traversal(self):
"""Test preventing path traversal."""
assert "/" not in sanitize_filename("../../../etc/passwd")
assert "\\" not in sanitize_filename("..\\..\\..\\windows\\system32")
def test_sanitize_special_characters(self):
"""Test removing special characters."""
result = sanitize_filename("file name with spaces!@#.jpg")
assert " " not in result or result == "file_name_with_spaces___.jpg"
def test_sanitize_long_filename(self):
"""Test truncating long filenames."""
long_name = "a" * 300 + ".jpg"
result = sanitize_filename(long_name)
assert len(result) <= 255
assert result.endswith(".jpg")
@pytest.mark.asyncio
class TestValidateImageFile:
"""Tests for image file validation."""
async def test_validate_empty_file(self):
"""Test rejection of empty files."""
mock_file = AsyncMock(spec=UploadFile)
mock_file.read = AsyncMock(return_value=b"")
mock_file.seek = AsyncMock()
mock_file.filename = "empty.jpg"
with pytest.raises(HTTPException) as exc:
await validate_image_file(mock_file)
assert exc.value.status_code == 400
assert "empty" in exc.value.detail.lower()
async def test_validate_file_too_large(self):
"""Test rejection of oversized files."""
# Create 60MB file
large_data = b"x" * (60 * 1024 * 1024)
mock_file = AsyncMock(spec=UploadFile)
mock_file.read = AsyncMock(return_value=large_data)
mock_file.seek = AsyncMock()
mock_file.filename = "large.jpg"
with pytest.raises(HTTPException) as exc:
await validate_image_file(mock_file)
assert exc.value.status_code == 413
assert "too large" in exc.value.detail.lower()
async def test_validate_invalid_extension(self):
"""Test rejection of invalid extensions."""
mock_file = AsyncMock(spec=UploadFile)
mock_file.read = AsyncMock(return_value=b"fake image data")
mock_file.seek = AsyncMock()
mock_file.filename = "document.pdf"
with pytest.raises(HTTPException) as exc:
await validate_image_file(mock_file)
assert exc.value.status_code == 400
assert "extension" in exc.value.detail.lower()