mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 08:31:08 -05:00
feat: complete monorepo structure with frontend and shared resources
- Add complete backend/ directory with full Django application - Add frontend/ directory with Vite + TypeScript setup ready for Next.js - Add comprehensive shared/ directory with: - Complete documentation and memory-bank archives - Media files and avatars (letters, park/ride images) - Deployment scripts and automation tools - Shared types and utilities - Add architecture/ directory with migration guides - Configure pnpm workspace for monorepo development - Update .gitignore to exclude .django_tailwind_cli/ build artifacts - Preserve all historical documentation in shared/docs/memory-bank/ - Set up proper structure for full-stack development with shared resources
This commit is contained in:
270
backend/apps/media/tests.py
Normal file
270
backend/apps/media/tests.py
Normal file
@@ -0,0 +1,270 @@
|
||||
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.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 apps.parks.models import Park, Company as Operator
|
||||
|
||||
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"""
|
||||
operator = Operator.objects.create(name="Test Operator")
|
||||
return Park.objects.create(
|
||||
name="Test Park", slug="test-park", operator=operator
|
||||
)
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user