mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 15:51:08 -05:00
272 lines
9.6 KiB
Python
272 lines
9.6 KiB
Python
from django.test import TestCase, override_settings
|
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.utils import timezone
|
|
from django.conf import settings
|
|
from django.test.utils import override_settings
|
|
from django.db import models
|
|
from datetime import datetime
|
|
from PIL import Image
|
|
import piexif # type: ignore
|
|
import io
|
|
import shutil
|
|
import tempfile
|
|
import os
|
|
import logging
|
|
from typing import Optional, Any, Generator, cast
|
|
from contextlib import contextmanager
|
|
from .models import Photo
|
|
from .storage import MediaStorage
|
|
from parks.models import Park
|
|
|
|
User = get_user_model()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
@override_settings(MEDIA_ROOT=tempfile.mkdtemp())
|
|
class PhotoModelTests(TestCase):
|
|
test_media_root: str
|
|
user: models.Model
|
|
park: Park
|
|
content_type: ContentType
|
|
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
super().setUpClass()
|
|
cls.test_media_root = settings.MEDIA_ROOT
|
|
|
|
@classmethod
|
|
def tearDownClass(cls) -> None:
|
|
try:
|
|
shutil.rmtree(cls.test_media_root, ignore_errors=True)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to clean up test media directory: {e}")
|
|
super().tearDownClass()
|
|
|
|
def setUp(self) -> None:
|
|
self.user = self._create_test_user()
|
|
self.park = self._create_test_park()
|
|
self.content_type = ContentType.objects.get_for_model(Park)
|
|
self._setup_test_directory()
|
|
|
|
def tearDown(self) -> None:
|
|
self._cleanup_test_directory()
|
|
Photo.objects.all().delete()
|
|
with self._reset_storage_state():
|
|
pass
|
|
|
|
def _create_test_user(self) -> models.Model:
|
|
"""Create a test user for the tests"""
|
|
return User.objects.create_user(
|
|
username='testuser',
|
|
password='testpass123'
|
|
)
|
|
|
|
def _create_test_park(self) -> Park:
|
|
"""Create a test park for the tests"""
|
|
return Park.objects.create(
|
|
name='Test Park',
|
|
slug='test-park'
|
|
)
|
|
|
|
def _setup_test_directory(self) -> None:
|
|
"""Set up test directory and clean any existing test files"""
|
|
try:
|
|
# Clean up any existing test park directory
|
|
test_park_dir = os.path.join(settings.MEDIA_ROOT, 'park', 'test-park')
|
|
if os.path.exists(test_park_dir):
|
|
shutil.rmtree(test_park_dir, ignore_errors=True)
|
|
|
|
# Create necessary directories
|
|
os.makedirs(test_park_dir, exist_ok=True)
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Failed to set up test directory: {e}")
|
|
raise
|
|
|
|
def _cleanup_test_directory(self) -> None:
|
|
"""Clean up test directories and files"""
|
|
try:
|
|
test_park_dir = os.path.join(settings.MEDIA_ROOT, 'park', 'test-park')
|
|
if os.path.exists(test_park_dir):
|
|
shutil.rmtree(test_park_dir, ignore_errors=True)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to clean up test directory: {e}")
|
|
|
|
@contextmanager
|
|
def _reset_storage_state(self) -> Generator[None, None, None]:
|
|
"""Safely reset storage state"""
|
|
try:
|
|
MediaStorage.reset_counters()
|
|
yield
|
|
finally:
|
|
MediaStorage.reset_counters()
|
|
|
|
def create_test_image_with_exif(self, date_taken: Optional[datetime] = None, filename: str = 'test.jpg') -> SimpleUploadedFile:
|
|
"""Helper method to create a test image with EXIF data"""
|
|
image = Image.new('RGB', (100, 100), color='red')
|
|
image_io = io.BytesIO()
|
|
|
|
# Save image first without EXIF
|
|
image.save(image_io, 'JPEG')
|
|
image_io.seek(0)
|
|
|
|
if date_taken:
|
|
# Create EXIF data
|
|
exif_dict = {
|
|
"0th": {},
|
|
"Exif": {
|
|
piexif.ExifIFD.DateTimeOriginal: date_taken.strftime("%Y:%m:%d %H:%M:%S").encode()
|
|
}
|
|
}
|
|
exif_bytes = piexif.dump(exif_dict)
|
|
|
|
# Insert EXIF into image
|
|
image_with_exif = io.BytesIO()
|
|
piexif.insert(exif_bytes, image_io.getvalue(), image_with_exif)
|
|
image_with_exif.seek(0)
|
|
image_data = image_with_exif.getvalue()
|
|
else:
|
|
image_data = image_io.getvalue()
|
|
|
|
return SimpleUploadedFile(
|
|
filename,
|
|
image_data,
|
|
content_type='image/jpeg'
|
|
)
|
|
|
|
def test_filename_normalization(self) -> None:
|
|
"""Test that filenames are properly normalized"""
|
|
with self._reset_storage_state():
|
|
# Test with various problematic filenames
|
|
test_cases = [
|
|
('test with spaces.jpg', 'test-park_1.jpg'),
|
|
('TEST_UPPER.JPG', 'test-park_2.jpg'),
|
|
('special@#chars.jpeg', 'test-park_3.jpg'),
|
|
('no-extension', 'test-park_4.jpg'),
|
|
('multiple...dots.jpg', 'test-park_5.jpg'),
|
|
('très_açaí.jpg', 'test-park_6.jpg'), # Unicode characters
|
|
]
|
|
|
|
for input_name, expected_suffix in test_cases:
|
|
photo = Photo.objects.create(
|
|
image=self.create_test_image_with_exif(filename=input_name),
|
|
uploaded_by=self.user,
|
|
content_type=self.content_type,
|
|
object_id=self.park.pk
|
|
)
|
|
|
|
# Check that the filename follows the normalized pattern
|
|
self.assertTrue(
|
|
photo.image.name.endswith(expected_suffix),
|
|
f"Expected filename to end with {expected_suffix}, got {photo.image.name}"
|
|
)
|
|
|
|
# Verify the path structure
|
|
expected_path = f"park/{self.park.slug}/"
|
|
self.assertTrue(
|
|
photo.image.name.startswith(expected_path),
|
|
f"Expected path to start with {expected_path}, got {photo.image.name}"
|
|
)
|
|
|
|
def test_sequential_filename_numbering(self) -> None:
|
|
"""Test that sequential files get proper numbering"""
|
|
with self._reset_storage_state():
|
|
# Create multiple photos and verify numbering
|
|
for i in range(1, 4):
|
|
photo = Photo.objects.create(
|
|
image=self.create_test_image_with_exif(),
|
|
uploaded_by=self.user,
|
|
content_type=self.content_type,
|
|
object_id=self.park.pk
|
|
)
|
|
|
|
expected_name = f"park/{self.park.slug}/test-park_{i}.jpg"
|
|
self.assertEqual(
|
|
photo.image.name,
|
|
expected_name,
|
|
f"Expected {expected_name}, got {photo.image.name}"
|
|
)
|
|
|
|
def test_exif_date_extraction(self) -> None:
|
|
"""Test EXIF date extraction from uploaded photos"""
|
|
test_date = datetime(2024, 1, 1, 12, 0, 0)
|
|
image_file = self.create_test_image_with_exif(test_date)
|
|
|
|
photo = Photo.objects.create(
|
|
image=image_file,
|
|
uploaded_by=self.user,
|
|
content_type=self.content_type,
|
|
object_id=self.park.pk
|
|
)
|
|
|
|
if photo.date_taken:
|
|
self.assertEqual(
|
|
photo.date_taken.strftime("%Y-%m-%d %H:%M:%S"),
|
|
test_date.strftime("%Y-%m-%d %H:%M:%S")
|
|
)
|
|
else:
|
|
self.skipTest("EXIF data extraction not supported in test environment")
|
|
|
|
def test_photo_without_exif(self) -> None:
|
|
"""Test photo upload without EXIF data"""
|
|
image_file = self.create_test_image_with_exif()
|
|
|
|
photo = Photo.objects.create(
|
|
image=image_file,
|
|
uploaded_by=self.user,
|
|
content_type=self.content_type,
|
|
object_id=self.park.pk
|
|
)
|
|
|
|
self.assertIsNone(photo.date_taken)
|
|
|
|
def test_default_caption(self) -> None:
|
|
"""Test default caption generation"""
|
|
photo = Photo.objects.create(
|
|
image=self.create_test_image_with_exif(),
|
|
uploaded_by=self.user,
|
|
content_type=self.content_type,
|
|
object_id=self.park.pk
|
|
)
|
|
|
|
expected_prefix = f"Uploaded by {cast(Any, self.user).username} on"
|
|
self.assertTrue(photo.caption.startswith(expected_prefix))
|
|
|
|
def test_primary_photo_toggle(self) -> None:
|
|
"""Test primary photo functionality"""
|
|
photo1 = Photo.objects.create(
|
|
image=self.create_test_image_with_exif(),
|
|
uploaded_by=self.user,
|
|
content_type=self.content_type,
|
|
object_id=self.park.pk,
|
|
is_primary=True
|
|
)
|
|
|
|
photo2 = Photo.objects.create(
|
|
image=self.create_test_image_with_exif(),
|
|
uploaded_by=self.user,
|
|
content_type=self.content_type,
|
|
object_id=self.park.pk,
|
|
is_primary=True
|
|
)
|
|
|
|
photo1.refresh_from_db()
|
|
photo2.refresh_from_db()
|
|
|
|
self.assertFalse(photo1.is_primary)
|
|
self.assertTrue(photo2.is_primary)
|
|
|
|
def test_date_taken_field(self) -> None:
|
|
"""Test date_taken field functionality"""
|
|
test_date = timezone.now()
|
|
photo = Photo.objects.create(
|
|
image=self.create_test_image_with_exif(),
|
|
uploaded_by=self.user,
|
|
content_type=self.content_type,
|
|
object_id=self.park.pk,
|
|
date_taken=test_date
|
|
)
|
|
|
|
self.assertEqual(photo.date_taken, test_date)
|