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:
10
core/tests/integration/test_health.py
Normal file
10
core/tests/integration/test_health.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import pytest
|
||||
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.django_db]
|
||||
|
||||
|
||||
def test_health_endpoint_reports_database_connection(client):
|
||||
response = client.get("/api/health")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "ok"
|
||||
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)
|
||||
23
core/tests/integration/test_soft_delete.py
Normal file
23
core/tests/integration/test_soft_delete.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import pytest
|
||||
|
||||
from apps.events.models import Event
|
||||
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.django_db]
|
||||
|
||||
|
||||
def test_soft_delete_hides_record_and_restore_reveals_it(event_factory):
|
||||
event = event_factory()
|
||||
event.delete()
|
||||
assert not Event.objects.filter(pk=event.pk).exists()
|
||||
assert Event.deleted_objects.filter(pk=event.pk).exists()
|
||||
assert Event.all_objects.get_queryset().dead().filter(pk=event.pk).exists()
|
||||
assert not Event.all_objects.get_queryset().alive().filter(pk=event.pk).exists()
|
||||
event.restore()
|
||||
assert Event.objects.filter(pk=event.pk).exists()
|
||||
|
||||
|
||||
def test_hard_delete_removes_row(event_factory):
|
||||
event = event_factory()
|
||||
Event.all_objects.filter(pk=event.pk).hard_delete()
|
||||
assert not Event.all_objects.filter(pk=event.pk).exists()
|
||||
Reference in New Issue
Block a user