@@ -1483,3 +1483,169 @@ def test_container_next_version(self) -> None:
14831483# Test that I can get a [PublishLog] history of a given container and its children, that includes changes made to the
14841484# child components while they were part of the container but excludes changes made to those children while they were
14851485# not part of the container. 🫣
1486+
1487+
1488+ class CrossEntityValidationTestCase (TestCase ):
1489+ """
1490+ Tests for validation gaps where API calls can corrupt state by mixing
1491+ entities/versions/packages that shouldn't be combined.
1492+ """
1493+ now : datetime
1494+ learning_package_1 : LearningPackage
1495+ learning_package_2 : LearningPackage
1496+
1497+ @classmethod
1498+ def setUpTestData (cls ) -> None :
1499+ cls .now = datetime (2024 , 6 , 15 , 12 , 0 , 0 , tzinfo = timezone .utc )
1500+ cls .learning_package_1 = publishing_api .create_learning_package (
1501+ "cross_entity_validation_lp_1" ,
1502+ "Cross-Entity Validation LP 1" ,
1503+ created = cls .now ,
1504+ )
1505+ cls .learning_package_2 = publishing_api .create_learning_package (
1506+ "cross_entity_validation_lp_2" ,
1507+ "Cross-Entity Validation LP 2" ,
1508+ created = cls .now ,
1509+ )
1510+
1511+ def test_set_draft_version_rejects_version_from_different_entity (self ) -> None :
1512+ """
1513+ set_draft_version() should reject a PublishableEntityVersion that
1514+ belongs to a different PublishableEntity.
1515+
1516+ If this validation is missing, entity_a's Draft will point to a version
1517+ that was defined for entity_b. This corrupts the publishing state:
1518+ component_a.versioning.draft would return component_b's data, and
1519+ publishing would propagate the wrong content.
1520+ """
1521+ entity_a = publishing_api .create_publishable_entity (
1522+ self .learning_package_1 .id ,
1523+ "entity_a" ,
1524+ created = self .now ,
1525+ created_by = None ,
1526+ )
1527+ entity_b = publishing_api .create_publishable_entity (
1528+ self .learning_package_1 .id ,
1529+ "entity_b" ,
1530+ created = self .now ,
1531+ created_by = None ,
1532+ )
1533+
1534+ # Create v1 for entity_a (draft_a -> v1)
1535+ publishing_api .create_publishable_entity_version (
1536+ entity_a .id ,
1537+ version_num = 1 ,
1538+ title = "Entity A v1" ,
1539+ created = self .now ,
1540+ created_by = None ,
1541+ )
1542+
1543+ # Create v1 and v2 for entity_b. After v2 is created, entity_b's
1544+ # draft points to v2, so v1 is "free" (no Draft points to it) and
1545+ # won't trigger a OneToOne constraint violation.
1546+ version_b_v1 = publishing_api .create_publishable_entity_version (
1547+ entity_b .id ,
1548+ version_num = 1 ,
1549+ title = "Entity B v1" ,
1550+ created = self .now ,
1551+ created_by = None ,
1552+ )
1553+ publishing_api .create_publishable_entity_version (
1554+ entity_b .id ,
1555+ version_num = 2 ,
1556+ title = "Entity B v2" ,
1557+ created = self .now ,
1558+ created_by = None ,
1559+ )
1560+
1561+ # Confirm version_b_v1 belongs to entity_b, not entity_a.
1562+ assert version_b_v1 .entity_id == entity_b .id
1563+ assert version_b_v1 .entity_id != entity_a .id
1564+
1565+ # This should raise an error because version_b_v1 belongs to entity_b,
1566+ # not entity_a. Without validation, this silently corrupts entity_a's
1567+ # draft to point to entity_b's content.
1568+ with pytest .raises ((ValidationError , ValueError )):
1569+ publishing_api .set_draft_version (entity_a .id , version_b_v1 .pk )
1570+
1571+ def test_publish_from_drafts_rejects_cross_package_drafts (self ) -> None :
1572+ """
1573+ publish_from_drafts() should reject drafts that don't belong to the
1574+ specified LearningPackage.
1575+
1576+ If this validation is missing, a PublishLog is created for LP 1 but
1577+ with PublishLogRecords referencing entities from LP 2. The Published
1578+ rows for LP 2's entities would point to records in LP 1's PublishLog,
1579+ corrupting the publish history for both packages.
1580+ """
1581+ # Create an entity in LP 2
1582+ entity_in_lp2 = publishing_api .create_publishable_entity (
1583+ self .learning_package_2 .id ,
1584+ "entity_in_lp2" ,
1585+ created = self .now ,
1586+ created_by = None ,
1587+ )
1588+ publishing_api .create_publishable_entity_version (
1589+ entity_in_lp2 .id ,
1590+ version_num = 1 ,
1591+ title = "Entity in LP2" ,
1592+ created = self .now ,
1593+ created_by = None ,
1594+ )
1595+
1596+ # Get drafts from LP 2
1597+ drafts_from_lp2 = Draft .objects .filter (
1598+ entity__learning_package_id = self .learning_package_2 .id
1599+ )
1600+ assert drafts_from_lp2 .exists ()
1601+
1602+ # This should raise an error because we're trying to publish LP 2's
1603+ # drafts under LP 1's PublishLog.
1604+ with pytest .raises ((ValidationError , ValueError )):
1605+ publishing_api .publish_from_drafts (
1606+ self .learning_package_1 .id ,
1607+ drafts_from_lp2 ,
1608+ )
1609+
1610+ def test_create_version_rejects_cross_package_dependencies (self ) -> None :
1611+ """
1612+ create_publishable_entity_version() should reject dependencies that
1613+ are from a different LearningPackage.
1614+
1615+ If this validation is missing, PublishableEntityVersionDependency rows
1616+ are created linking entities across packages. The side-effect machinery
1617+ would then propagate draft/publish changes across LearningPackage
1618+ boundaries, creating DraftChangeLogRecords and PublishLogRecords in the
1619+ wrong package's logs.
1620+ """
1621+ entity_in_lp1 = publishing_api .create_publishable_entity (
1622+ self .learning_package_1 .id ,
1623+ "entity_in_lp1" ,
1624+ created = self .now ,
1625+ created_by = None ,
1626+ )
1627+ entity_in_lp2 = publishing_api .create_publishable_entity (
1628+ self .learning_package_2 .id ,
1629+ "dep_entity_in_lp2" ,
1630+ created = self .now ,
1631+ created_by = None ,
1632+ )
1633+ publishing_api .create_publishable_entity_version (
1634+ entity_in_lp2 .id ,
1635+ version_num = 1 ,
1636+ title = "Dependency in LP2" ,
1637+ created = self .now ,
1638+ created_by = None ,
1639+ )
1640+
1641+ # This should raise an error because entity_in_lp2 is from a
1642+ # different LearningPackage than entity_in_lp1.
1643+ with pytest .raises ((ValidationError , ValueError )):
1644+ publishing_api .create_publishable_entity_version (
1645+ entity_in_lp1 .id ,
1646+ version_num = 1 ,
1647+ title = "Entity in LP1 with cross-package dep" ,
1648+ created = self .now ,
1649+ created_by = None ,
1650+ dependencies = [entity_in_lp2 .id ],
1651+ )
0 commit comments