Protect published snapshots and concurrent edits
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import io
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from threading import Barrier
|
||||
|
||||
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 django.db import connection, connections, close_old_connections
|
||||
from PIL import Image
|
||||
|
||||
from apps.content.models import EditorialPage, MediaAsset, PageRevision, PublicationAudit, SiteIdentity
|
||||
@@ -103,6 +106,34 @@ def test_stale_publish_is_rejected_without_swapping_pointer():
|
||||
assert second.status == "draft"
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.skipif(connection.vendor != "postgresql", reason="SQLite has no row-level select_for_update locking")
|
||||
def test_concurrent_publish_only_one_revision_wins():
|
||||
page = EditorialPage.objects.create(slug="home", page_kind="sections")
|
||||
user = actor("chief", superuser=True)
|
||||
drafts = [save_page_draft(user, page, page_fields(title), home_sections()) for title in ("First", "Second")]
|
||||
barrier = Barrier(3)
|
||||
|
||||
def attempt(revision_id):
|
||||
close_old_connections()
|
||||
barrier.wait()
|
||||
try:
|
||||
publish_page(user, PageRevision.objects.get(pk=revision_id), expected_published_id=None)
|
||||
return "published"
|
||||
except PublishConflict:
|
||||
return "conflict"
|
||||
finally:
|
||||
connections.close_all()
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as pool:
|
||||
futures = [pool.submit(attempt, draft.pk) for draft in drafts]
|
||||
barrier.wait()
|
||||
outcomes = [future.result(timeout=15) for future in futures]
|
||||
assert sorted(outcomes) == ["conflict", "published"]
|
||||
page.refresh_from_db()
|
||||
assert page.published_revision_id in {draft.pk for draft in drafts}
|
||||
|
||||
|
||||
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)
|
||||
@@ -181,3 +212,7 @@ def test_revision_sections_and_audit_are_immutable():
|
||||
audit.operation = "publish"
|
||||
with pytest.raises(ValidationError):
|
||||
audit.save()
|
||||
published = publish_page(user, PageRevision.objects.get(pk=draft.pk), expected_published_id=None)
|
||||
published.status = "draft"
|
||||
with pytest.raises(ValidationError):
|
||||
published.save()
|
||||
|
||||
Reference in New Issue
Block a user