Files
guilan-ace-backend/apps/content/tests/integration/test_services.py

184 lines
8.6 KiB
Python

import io
import pytest
from django.contrib.auth.models import Group, Permission
from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied
from django.core.files.uploadedfile import SimpleUploadedFile
from PIL import Image
from apps.content.models import EditorialPage, MediaAsset, PageRevision, PublicationAudit, SiteIdentity
from apps.content.services import (
PublishConflict,
publish_page,
publish_site,
restore_page,
restore_site,
save_page_draft,
save_site_draft,
)
from apps.users.models import User
pytestmark = [pytest.mark.django_db, pytest.mark.integration]
def actor(name, *permissions, staff=True, superuser=False):
user = User.objects.create_user(username=name, mobile=f"0912{User.objects.count():07d}", password="TestPass123!")
user.is_staff = staff
user.is_superuser = superuser
user.save(update_fields=["is_staff", "is_superuser"])
user.user_permissions.add(*Permission.objects.filter(content_type__app_label="content", codename__in=permissions))
return user
def page_fields(title="Home"):
return {"title": title, "intro": "Introduction", "seo_title": title, "seo_description": "Description"}
def home_sections():
return [
{"key": "hero", "section_type": "hero", "position": 0, "heading": "Welcome", "body": "Hello"},
{"key": "featured_events", "section_type": "featured_events", "position": 1, "heading": "Events", "all_link_label": "All events"},
{"key": "featured_posts", "section_type": "featured_posts", "position": 2, "heading": "Posts", "all_link_label": "All posts"},
{"key": "participation", "section_type": "participation", "position": 3, "heading": "Join", "body": "Join us"},
]
def site_fields():
return {
"official_name": "Association",
"short_name": "ACE",
"navigation": [{"route_id": route, "label": route, "position": index, "visible": True} for index, route in enumerate(("home", "about", "events", "blog"))],
"contacts": [],
}
def test_role_matrix_and_audit_for_page():
page = EditorialPage.objects.create(slug="home", page_kind="sections")
outsider = actor("outsider", staff=False)
staff = actor("staff")
editor = actor("editor", "change_content_draft", "preview_content_draft")
nonstaff_editor = actor("nonstaff", "change_content_draft", staff=False)
publisher = actor("publisher", "change_content_draft", "preview_content_draft", "publish_content", "restore_content")
for denied in (outsider, staff, nonstaff_editor):
with pytest.raises(PermissionDenied):
save_page_draft(denied, page, page_fields(), home_sections())
draft = save_page_draft(editor, page, page_fields(), home_sections())
assert draft.number == 1 and draft.status == "draft"
with pytest.raises(PermissionDenied):
publish_page(editor, draft, expected_published_id=None)
published = publish_page(publisher, draft, expected_published_id=None)
page.refresh_from_db()
assert page.published_revision_id == published.id
assert published.status == "published" and published.published_by == publisher
assert list(PublicationAudit.objects.values_list("operation", flat=True)) == ["create_draft", "publish"]
def test_edit_creates_revision_and_restore_preserves_history():
page = EditorialPage.objects.create(slug="home", page_kind="sections")
user = actor("chief", superuser=True)
first = save_page_draft(user, page, page_fields("First"), home_sections())
publish_page(user, first, expected_published_id=None)
second = save_page_draft(user, page, page_fields("Second"), home_sections(), base_revision=first)
publish_page(user, second, expected_published_id=first.id)
restored = restore_page(user, page, first, expected_published_id=second.id)
assert restored.number == 3 and restored.title == "First"
assert restored.content_checksum == first.content_checksum
assert PageRevision.objects.get(pk=second.pk).title == "Second"
assert page.revisions.count() == 3
assert list(PublicationAudit.objects.values_list("operation", flat=True)) == ["create_draft", "publish", "update_draft", "publish", "restore"]
def test_stale_publish_is_rejected_without_swapping_pointer():
page = EditorialPage.objects.create(slug="home", page_kind="sections")
user = actor("chief", superuser=True)
first = save_page_draft(user, page, page_fields("First"), home_sections())
second = save_page_draft(user, page, page_fields("Second"), home_sections())
publish_page(user, first, expected_published_id=None)
with pytest.raises(PublishConflict):
publish_page(user, second, expected_published_id=None)
page.refresh_from_db()
assert page.published_revision_id == first.id
assert second.status == "draft"
def test_publish_rejects_incomplete_page_and_leaves_no_public_content():
page = EditorialPage.objects.create(slug="home", page_kind="sections")
user = actor("chief", superuser=True)
draft = save_page_draft(user, page, page_fields(), home_sections()[:-1])
with pytest.raises(ValidationError):
publish_page(user, draft, expected_published_id=None)
page.refresh_from_db()
assert page.published_revision_id is None
def test_site_draft_and_publish_require_distinct_permissions():
site = SiteIdentity.objects.create()
editor = actor("editor", "change_content_draft")
publisher = actor("publisher", "publish_content")
draft = save_site_draft(editor, site, site_fields())
with pytest.raises(PermissionDenied):
publish_site(editor, draft, expected_published_id=None)
publish_site(publisher, draft, expected_published_id=None)
site.refresh_from_db()
assert site.published_revision_id == draft.id
def test_restore_site_copies_media_reference_and_creates_new_revision(settings, tmp_path):
settings.MEDIA_ROOT = tmp_path
buffer = io.BytesIO()
Image.new("RGB", (20, 20)).save(buffer, format="PNG")
logo = MediaAsset.objects.create(file=SimpleUploadedFile("logo.png", buffer.getvalue(), content_type="image/png"), alt="Logo", source="owner", license="owned", approved=True)
site = SiteIdentity.objects.create()
user = actor("chief", superuser=True)
fields = site_fields() | {"logo": logo, "logo_alt": "Association logo"}
first = save_site_draft(user, site, fields)
publish_site(user, first, expected_published_id=None)
second = save_site_draft(user, site, site_fields() | {"official_name": "New name"})
publish_site(user, second, expected_published_id=first.id)
restored = restore_site(user, site, first, expected_published_id=second.id)
site.refresh_from_db()
assert site.published_revision_id == restored.id
assert restored.logo_id == logo.id and restored.number == 3
assert restored.content_checksum == first.content_checksum
def test_unapproved_media_cannot_be_published(settings, tmp_path):
settings.MEDIA_ROOT = tmp_path
buffer = io.BytesIO()
Image.new("RGB", (20, 20)).save(buffer, format="PNG")
logo = MediaAsset.objects.create(file=SimpleUploadedFile("logo.png", buffer.getvalue(), content_type="image/png"), alt="Logo", source="owner", license="owned")
site = SiteIdentity.objects.create()
user = actor("chief", superuser=True)
draft = save_site_draft(user, site, site_fields() | {"logo": logo, "logo_alt": "Association logo"})
with pytest.raises(ValidationError):
publish_site(user, draft, expected_published_id=None)
site.refresh_from_db()
assert site.published_revision_id is None
def test_groups_are_provisioned_without_assigning_accounts():
editor = Group.objects.get(name="content_editor")
publisher = Group.objects.get(name="content_publisher")
assert set(editor.permissions.values_list("codename", flat=True)) >= {"change_content_draft", "preview_content_draft"}
assert set(publisher.permissions.values_list("codename", flat=True)) >= {"change_content_draft", "preview_content_draft", "publish_content", "restore_content"}
assert editor.user_set.count() == 0 and publisher.user_set.count() == 0
def test_revision_sections_and_audit_are_immutable():
page = EditorialPage.objects.create(slug="home", page_kind="sections")
user = actor("chief", superuser=True)
draft = save_page_draft(user, page, page_fields(), home_sections())
draft.title = "Changed in place"
with pytest.raises(ValidationError):
draft.save()
section = draft.sections.first()
section.heading = "Changed in place"
with pytest.raises(ValidationError):
section.save()
audit = PublicationAudit.objects.first()
audit.operation = "publish"
with pytest.raises(ValidationError):
audit.save()