test: migrate backend suites to pytest with module coverage gate
Split backend tests into unit and integration suites, add isolated fixtures and coverage enforcement, and update CI and test documentation. Cover current authentication, content, notification, payment, media, and admin flows; fix behaviors revealed by the new tests. Validate both suites and the per-module gate against local PostgreSQL.
This commit is contained in:
95
core/tests/integration/test_media.py
Normal file
95
core/tests/integration/test_media.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import pytest
|
||||
import io
|
||||
from datetime import timedelta
|
||||
from PIL import Image
|
||||
from django.core.management import call_command
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import RequestFactory
|
||||
from django.utils import timezone
|
||||
from apps.blog.api.schemas import AuthorSchema, PostListSchema
|
||||
from apps.blog.models import Post
|
||||
from apps.events.api.schemas import EventGallerySchema, EventListSchema
|
||||
from apps.events.models import Event
|
||||
from apps.gallery.api.schemas import GallerySchema
|
||||
from apps.gallery.models import Gallery
|
||||
from apps.users.api.schemas import UserProfileSchema
|
||||
from apps.users.models import User
|
||||
from core.media import BLUR_VARIANT, PREVIEW_VARIANT, THUMBNAIL_VARIANT, delete_image_derivatives, derivative_name, process_public_image
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.integration
|
||||
class TestMediaProcessingTests:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _media_root(self, settings, tmp_path):
|
||||
self.media_root = str(tmp_path)
|
||||
settings.MEDIA_ROOT = self.media_root
|
||||
settings.MEDIA_URL = '/media/'
|
||||
|
||||
def setup_method(self):
|
||||
self.user = User.objects.create_user(username='media-user', email='media@example.com', mobile='09123456789', password='TestPass123!')
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def _upload(self, name: str, *, mode: str='RGB', fmt: str='JPEG') -> SimpleUploadedFile:
|
||||
buffer = io.BytesIO()
|
||||
Image.new(mode, (2400, 1800), color=(20, 80, 200)).save(buffer, format=fmt)
|
||||
buffer.seek(0)
|
||||
return SimpleUploadedFile(name, buffer.read(), content_type=f'image/{fmt.lower()}')
|
||||
|
||||
def test_process_public_image_generates_derivatives_for_jpeg(self):
|
||||
gallery = Gallery.objects.create(title='JPEG image', uploaded_by=self.user, image=self._upload('photo.jpg', fmt='JPEG'))
|
||||
result = process_public_image(gallery.image, 'gallery', force=True)
|
||||
assert result.processed
|
||||
assert gallery.image.storage.exists(derivative_name(gallery.image.name, THUMBNAIL_VARIANT))
|
||||
assert gallery.image.storage.exists(derivative_name(gallery.image.name, PREVIEW_VARIANT))
|
||||
assert gallery.image.storage.exists(derivative_name(gallery.image.name, BLUR_VARIANT))
|
||||
|
||||
def test_process_public_image_generates_derivatives_for_png(self):
|
||||
gallery = Gallery.objects.create(title='PNG image', uploaded_by=self.user, image=self._upload('photo.png', mode='RGBA', fmt='PNG'))
|
||||
result = process_public_image(gallery.image, 'gallery', force=True)
|
||||
assert result.width == 2400
|
||||
assert result.height == 1800
|
||||
assert gallery.image.storage.exists(derivative_name(gallery.image.name, PREVIEW_VARIANT))
|
||||
|
||||
def test_delete_image_derivatives_removes_sidecars(self):
|
||||
gallery = Gallery.objects.create(title='Delete derivatives', uploaded_by=self.user, image=self._upload('cleanup.jpg', fmt='JPEG'))
|
||||
process_public_image(gallery.image, 'gallery', force=True)
|
||||
delete_image_derivatives(gallery.image, 'gallery')
|
||||
assert not gallery.image.storage.exists(derivative_name(gallery.image.name, THUMBNAIL_VARIANT))
|
||||
assert not gallery.image.storage.exists(derivative_name(gallery.image.name, PREVIEW_VARIANT))
|
||||
assert not gallery.image.storage.exists(derivative_name(gallery.image.name, BLUR_VARIANT))
|
||||
|
||||
def test_replacing_image_removes_previous_sidecars(self):
|
||||
gallery = Gallery.objects.create(title='Replace derivatives', uploaded_by=self.user, image=self._upload('replace-first.jpg', fmt='JPEG'))
|
||||
first_name = gallery.image.name
|
||||
gallery.image = self._upload('replace-second.jpg', fmt='JPEG')
|
||||
gallery.save()
|
||||
assert not gallery.image.storage.exists(derivative_name(first_name, THUMBNAIL_VARIANT))
|
||||
assert not gallery.image.storage.exists(derivative_name(first_name, PREVIEW_VARIANT))
|
||||
assert not gallery.image.storage.exists(derivative_name(first_name, BLUR_VARIANT))
|
||||
|
||||
def test_backfill_command_is_idempotent(self):
|
||||
gallery = Gallery(title='Backfill image', uploaded_by=self.user)
|
||||
gallery._defer_image_processing = True
|
||||
gallery.image = self._upload('backfill.jpg', fmt='JPEG')
|
||||
gallery.save()
|
||||
first = io.StringIO()
|
||||
second = io.StringIO()
|
||||
call_command('backfill_media_derivatives', stdout=first)
|
||||
call_command('backfill_media_derivatives', stdout=second)
|
||||
assert 'processed=1' in first.getvalue()
|
||||
assert 'skipped=1' in second.getvalue()
|
||||
|
||||
def test_schema_resolvers_expose_derivative_urls(self):
|
||||
self.user.profile_picture.save('profile.jpg', self._upload('profile.jpg', fmt='JPEG'))
|
||||
gallery = Gallery.objects.create(title='Schema image', uploaded_by=self.user, image=self._upload('schema-gallery.jpg', fmt='JPEG'))
|
||||
event = Event.objects.create(title='Schema event', description='Event description', start_time=timezone.now(), end_time=timezone.now() + timedelta(hours=2), featured_image=self._upload('schema-event.jpg', fmt='JPEG'))
|
||||
post = Post.objects.create(title='Schema post', content='Post body', author=self.user, featured_image=self._upload('schema-post.jpg', fmt='JPEG'), status=Post.StatusChoices.PUBLISHED)
|
||||
context = {'request': self.factory.get('/')}
|
||||
assert derivative_name(event.featured_image.name, THUMBNAIL_VARIANT) in EventListSchema.resolve_absolute_featured_image_thumbnail_url(event, context)
|
||||
assert derivative_name(event.featured_image.name, PREVIEW_VARIANT) in EventListSchema.resolve_absolute_featured_image_preview_url(event, context)
|
||||
assert derivative_name(gallery.image.name, PREVIEW_VARIANT) in EventGallerySchema.resolve_absolute_image_preview_url(gallery, context)
|
||||
assert derivative_name(gallery.image.name, BLUR_VARIANT) in GallerySchema.resolve_absolute_image_blur_url(gallery, context)
|
||||
assert derivative_name(post.featured_image.name, THUMBNAIL_VARIANT) in PostListSchema.resolve_absolute_featured_image_thumbnail_url(post, context)
|
||||
assert derivative_name(self.user.profile_picture.name, THUMBNAIL_VARIANT) in AuthorSchema.resolve_profile_picture_thumbnail_url(self.user, context)
|
||||
assert derivative_name(self.user.profile_picture.name, PREVIEW_VARIANT) in UserProfileSchema.resolve_profile_picture_preview_url(self.user, context)
|
||||
Reference in New Issue
Block a user