Add validated editorial content schema

This commit is contained in:
2026-09-27 09:26:34 +03:30
parent 3922dbf673
commit 3bc9de2367
10 changed files with 605 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
import io
import pytest
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db import IntegrityError, transaction
from PIL import Image
from apps.content.models import EditorialPage, MediaAsset, PageRevision, PageSection, SiteIdentity, SiteRevision
pytestmark = [pytest.mark.django_db, pytest.mark.integration]
def test_single_site_and_fixed_page_slugs():
SiteIdentity.objects.create()
with pytest.raises(IntegrityError), transaction.atomic():
SiteIdentity.objects.create()
page = EditorialPage(slug="home", page_kind="sections")
page.full_clean()
page.save()
with pytest.raises(ValidationError):
EditorialPage(slug="other", page_kind="sections").full_clean()
with pytest.raises(ValidationError):
EditorialPage(slug="events", page_kind="sections").full_clean()
def test_site_revision_rejects_unsafe_text_and_contact_links():
site = SiteIdentity.objects.create()
navigation = [{"route_id": route, "label": route, "position": index, "visible": True} for index, route in enumerate(("home", "about", "events", "blog"))]
revision = SiteRevision(site=site, number=1, official_name="<script>alert(1)</script>", short_name="ACE", navigation=navigation)
with pytest.raises(ValidationError):
revision.full_clean()
revision.official_name = "Association"
revision.contacts = [{"kind": "telegram", "label": "Join", "url": "javascript:alert(1)", "position": 0, "visible": True}]
with pytest.raises(ValidationError):
revision.full_clean()
revision.contacts = [{"kind": ["telegram"], "label": "Join", "url": "https://t.me/example", "position": 0, "visible": True}]
with pytest.raises(ValidationError):
revision.full_clean()
revision.contacts = [{"kind": "telegram", "label": "Join", "url": "https://t.me/example", "position": 0, "visible": True}]
revision.full_clean()
def test_section_registry_and_plain_text():
page = EditorialPage.objects.create(slug="home", page_kind="sections")
revision = PageRevision.objects.create(page=page, number=1, title="Home", seo_title="Home", seo_description="About us")
section = PageSection(revision=revision, key="hero", section_type="hero", position=0, heading="Welcome", body="<img src=x onerror=alert(1)>")
with pytest.raises(ValidationError):
section.full_clean()
section.body = "Plain introduction"
section.full_clean()
section.image_alt = "Orphan alt"
with pytest.raises(ValidationError):
section.full_clean()
section.image_alt = ""
section.primary_action = {"label": "Go", "route_id": "javascript:alert(1)"}
with pytest.raises(ValidationError):
section.full_clean()
section.primary_action = {"label": "Go", "route_id": ["about"]}
with pytest.raises(ValidationError):
section.full_clean()
with pytest.raises(ValidationError):
PageSection(revision=revision, key="mission", section_type="mission", position=1, heading="Mission").full_clean()
def test_revision_numbers_and_section_keys_are_unique():
page = EditorialPage.objects.create(slug="about", page_kind="sections")
PageRevision.objects.create(page=page, number=1, title="About", seo_title="About", seo_description="Association")
with pytest.raises(IntegrityError), transaction.atomic():
PageRevision.objects.create(page=page, number=1, title="Again", seo_title="Again", seo_description="Association")
def test_section_entries_reject_markup_and_excess():
page = EditorialPage.objects.create(slug="about", page_kind="sections")
revision = PageRevision.objects.create(page=page, number=1, title="About", seo_title="About", seo_description="Association")
section = PageSection(revision=revision, key="faq", section_type="faq", position=1, heading="Questions", entries=[{"title": "Question", "text": "<script>alert(1)</script>"}])
with pytest.raises(ValidationError):
section.full_clean()
section.entries = [{"title": f"Question {index}", "text": "Answer"} for index in range(21)]
with pytest.raises(ValidationError):
section.full_clean()
def test_media_rejects_invalid_signature_and_oversize(settings, tmp_path):
settings.MEDIA_ROOT = tmp_path
bad = MediaAsset(file=SimpleUploadedFile("fake.png", b"not an image", content_type="image/png"), alt="Logo", source="owner", license="owned")
with pytest.raises(ValidationError):
bad.full_clean()
buffer = io.BytesIO()
Image.new("RGB", (10, 10)).save(buffer, format="PNG")
good = MediaAsset(file=SimpleUploadedFile("logo.png", buffer.getvalue(), content_type="image/png"), alt="Association logo", source="owner", license="owned")
good.full_clean()
good.save()
assert good.width == 10 and good.height == 10
huge = MediaAsset(file=SimpleUploadedFile("huge.png", b"x" * (5 * 1024 * 1024 + 1), content_type="image/png"), alt="Huge", source="owner", license="owned")
with pytest.raises(ValidationError):
huge.full_clean()
mismatch = MediaAsset(file=SimpleUploadedFile("logo.jpg", buffer.getvalue(), content_type="image/jpeg"), alt="Logo", source="owner", license="owned")
with pytest.raises(ValidationError):
mismatch.full_clean()