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