Skip to content

Commit 160a649

Browse files
test: verify that bulk event is not emitted on transaction rollback
1 parent dc93d73 commit 160a649

3 files changed

Lines changed: 66 additions & 10 deletions

File tree

src/openedx_content/applets/publishing/signals.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ class DraftChangeLogEventData:
7373
"""
7474
The draft version of one or more entities in a `LearningPackage` has changed.
7575
76+
This is emitted for when the first version of an entity is created, when a new
77+
version of an entity is created (i.e. the entity is modified), when an entity is
78+
reverted to an old version, or when an entity is deleted. (All referring to the
79+
draft version of the entity.)
80+
7681
This is a low-level batch event. It does not have any course or library context
7782
information available. It does not distinguish between Containers, Components,
7883
or other entity types.

tests/openedx_content/applets/publishing/test_signals.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
"""
44

55
from datetime import datetime, timezone
6+
from typing import Any
67

78
import pytest
8-
from django.db import transaction
99

1010
from openedx_content import api
11-
from tests.utils import capture_events
11+
from tests.utils import abort_transaction, capture_events
1212

1313
pytestmark = pytest.mark.django_db(transaction=True)
1414
now_time = datetime.now(tz=timezone.utc)
@@ -64,14 +64,10 @@ def test_single_entity_changed_abort() -> None:
6464
entity = api.create_publishable_entity(learning_package.id, key="entity1", created=now_time, created_by=None)
6565

6666
with capture_events(expected_count=0):
67-
try:
68-
with transaction.atomic():
69-
api.create_publishable_entity_version(
70-
entity.id, version_num=1, title="Entity 1 V1", created=now_time, created_by=None
71-
)
72-
raise DeliberateRollbackException()
73-
except DeliberateRollbackException:
74-
pass
67+
with abort_transaction():
68+
api.create_publishable_entity_version(
69+
entity.id, version_num=1, title="Entity 1 V1", created=now_time, created_by=None
70+
)
7571

7672

7773
def test_multiple_entites_changed(admin_user) -> None:
@@ -121,3 +117,33 @@ def test_multiple_entites_changed(admin_user) -> None:
121117
api.signals.ChangeLogRecordData(entity_id=entity3.id, old_version=1, new_version=None),
122118
]
123119
assert event.kwargs["metadata"].time == now_time
120+
121+
122+
def test_multiple_entites_change_aborted() -> None:
123+
"""
124+
Test that LEARNING_PACKAGE_ENTITIES_CHANGED is NOT emitted when we roll back
125+
a transaction that would have modified multiple entities in a bulk change.
126+
"""
127+
learning_package = api.create_learning_package(key="lp1", title="Test LP 📦")
128+
created_args: dict[str, Any] = {"created": now_time, "created_by": None}
129+
130+
# Entity 1 will have no initial version:
131+
entity1 = api.create_publishable_entity(learning_package.id, key="entity1", **created_args)
132+
# Entity 2 will have an initial version:
133+
entity2 = api.create_publishable_entity(learning_package.id, key="entity2", **created_args)
134+
api.create_publishable_entity_version(entity2.id, version_num=1, title="Entity 2 V1", **created_args)
135+
# Entity 3 will have an initial version that later gets deleted:
136+
entity3 = api.create_publishable_entity(learning_package.id, key="entity3", **created_args)
137+
api.create_publishable_entity_version(entity3.id, version_num=1, title="Entity 3 V1", **created_args)
138+
139+
with capture_events(expected_count=0):
140+
with abort_transaction():
141+
with api.bulk_draft_changes_for(learning_package.id, changed_by=None, changed_at=now_time):
142+
# Note: the 'created_args' values below get ignored because of the bulk context.
143+
# Create two versions of entity1:
144+
api.create_publishable_entity_version(entity1.id, version_num=1, title="Entity 1 V1", **created_args)
145+
api.create_publishable_entity_version(entity1.id, version_num=2, title="Entity 1 V2", **created_args)
146+
# Create a version 2 of entity 2:
147+
api.create_publishable_entity_version(entity2.id, version_num=2, title="Entity 2 V2", **created_args)
148+
# Delete entity 3:
149+
api.set_draft_version(entity3.id, None, set_at=now_time, set_by=None)

tests/utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from dataclasses import dataclass
99
from typing import Generator
1010

11+
from django.db import transaction
1112
from openedx_events.tooling import OpenEdxPublicSignal # type: ignore[import-untyped]
1213

1314

@@ -77,3 +78,27 @@ def receiver(sender, **kwargs): # pylint: disable=unused-argument
7778
assert len(captured) == expected_count, (
7879
f"Expected {expected_count} event(s), got {len(captured)}: {[e.signal for e in captured]}"
7980
)
81+
82+
83+
class DeliberateRollbackException(Exception):
84+
"""Exception used to deliberately cancel and roll back a DB transaction"""
85+
86+
87+
@contextmanager
88+
def abort_transaction() -> Generator[None, None, None]:
89+
"""
90+
Context manager that wraps the block in a transaction that gets rolled back.
91+
92+
Example usage::
93+
94+
with abort_transaction():
95+
api.do_something(...)
96+
97+
assert nothing was done
98+
"""
99+
try:
100+
with transaction.atomic():
101+
yield
102+
raise DeliberateRollbackException
103+
except DeliberateRollbackException:
104+
pass

0 commit comments

Comments
 (0)