Add governed organization template upgrades
This commit is contained in:
@@ -15,6 +15,7 @@ from govoplan_organizations.backend.db.models import (
|
||||
OrganizationModelInstantiation,
|
||||
OrganizationModelTemplate,
|
||||
OrganizationModelTemplateVersion,
|
||||
OrganizationModelUpgrade,
|
||||
OrganizationRelation,
|
||||
OrganizationRelationType,
|
||||
OrganizationStructure,
|
||||
@@ -26,6 +27,13 @@ from govoplan_organizations.backend.templates import (
|
||||
canonical_template_definition,
|
||||
instantiate_template_version,
|
||||
)
|
||||
from govoplan_organizations.backend.upgrades import (
|
||||
OrganizationUpgradeError,
|
||||
apply_model_upgrade,
|
||||
cancel_model_upgrade,
|
||||
create_model_upgrade_preview,
|
||||
current_model_instantiation,
|
||||
)
|
||||
|
||||
|
||||
TABLES = [
|
||||
@@ -39,6 +47,7 @@ TABLES = [
|
||||
OrganizationFunction.__table__,
|
||||
OrganizationRelation.__table__,
|
||||
OrganizationModelInstantiation.__table__,
|
||||
OrganizationModelUpgrade.__table__,
|
||||
]
|
||||
|
||||
|
||||
@@ -216,6 +225,248 @@ class OrganizationModelTemplateTests(unittest.TestCase):
|
||||
):
|
||||
canonical_template_definition(definition)
|
||||
|
||||
def test_three_way_upgrade_previews_additions_and_local_divergence(self) -> None:
|
||||
template, source, target = self._instantiated_upgrade_fixture(
|
||||
mutate_target=lambda definition: definition.units.append(
|
||||
definition.units[1].model_copy(
|
||||
update={
|
||||
"slug": "service-office",
|
||||
"name": "Service office",
|
||||
"parent_slug": "municipality",
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
office = (
|
||||
self.session.query(OrganizationUnit)
|
||||
.filter_by(tenant_id="tenant-1", slug="office")
|
||||
.one()
|
||||
)
|
||||
office.name = "Tenant-specific office"
|
||||
self.session.commit()
|
||||
|
||||
preview = create_model_upgrade_preview(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
target_version=target,
|
||||
actor_account_id="account-1",
|
||||
idempotency_key="preview-additive",
|
||||
)
|
||||
self.session.commit()
|
||||
|
||||
self.assertEqual(1, preview.preview["counts"]["compatible_addition"])
|
||||
self.assertEqual(1, preview.preview["counts"]["local_divergence"])
|
||||
self.assertEqual(0, preview.preview["requires_decisions"])
|
||||
applied, instantiation = apply_model_upgrade(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
upgrade_id=preview.id,
|
||||
expected_revision=1,
|
||||
decisions={},
|
||||
actor_account_id="account-2",
|
||||
)
|
||||
self.session.commit()
|
||||
|
||||
self.assertEqual("applied", applied.status)
|
||||
self.assertEqual(target.id, instantiation.template_version_id)
|
||||
self.assertEqual(
|
||||
"Tenant-specific office",
|
||||
self.session.query(OrganizationUnit)
|
||||
.filter_by(tenant_id="tenant-1", slug="office")
|
||||
.one()
|
||||
.name,
|
||||
)
|
||||
self.assertIsNotNone(
|
||||
self.session.query(OrganizationUnit)
|
||||
.filter_by(tenant_id="tenant-1", slug="service-office")
|
||||
.one_or_none()
|
||||
)
|
||||
self.assertEqual("superseded", source.status)
|
||||
self.assertEqual(instantiation.id, current_model_instantiation(self.session, tenant_id="tenant-1").id)
|
||||
self.assertEqual(template.id, instantiation.template_id)
|
||||
|
||||
def test_divergent_upgrade_requires_bounded_decision_and_detects_stale_state(self) -> None:
|
||||
_template, _source, target = self._instantiated_upgrade_fixture(
|
||||
mutate_target=lambda definition: setattr(
|
||||
definition.units[1], "name", "Template office"
|
||||
)
|
||||
)
|
||||
office = self.session.query(OrganizationUnit).filter_by(
|
||||
tenant_id="tenant-1", slug="office"
|
||||
).one()
|
||||
office.name = "Local office"
|
||||
self.session.commit()
|
||||
preview = create_model_upgrade_preview(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
target_version=target,
|
||||
actor_account_id="account-1",
|
||||
idempotency_key="preview-divergent",
|
||||
)
|
||||
self.session.commit()
|
||||
conflict = next(
|
||||
entry
|
||||
for entry in preview.preview["entries"]
|
||||
if entry["id"] == "units:office"
|
||||
)
|
||||
self.assertTrue(conflict["requires_decision"])
|
||||
with self.assertRaisesRegex(OrganizationUpgradeError, "decision is required"):
|
||||
apply_model_upgrade(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
upgrade_id=preview.id,
|
||||
expected_revision=1,
|
||||
decisions={},
|
||||
actor_account_id="account-2",
|
||||
)
|
||||
self.session.rollback()
|
||||
|
||||
office = self.session.query(OrganizationUnit).filter_by(
|
||||
tenant_id="tenant-1", slug="office"
|
||||
).one()
|
||||
office.description = "Changed after preview"
|
||||
self.session.commit()
|
||||
with self.assertRaisesRegex(OrganizationUpgradeError, "changed after this preview"):
|
||||
apply_model_upgrade(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
upgrade_id=preview.id,
|
||||
expected_revision=1,
|
||||
decisions={"units:office": {"action": "use_target"}},
|
||||
actor_account_id="account-2",
|
||||
)
|
||||
|
||||
def test_divergent_upgrade_applies_explicit_target_decision(self) -> None:
|
||||
_template, _source, target = self._instantiated_upgrade_fixture(
|
||||
mutate_target=lambda definition: setattr(
|
||||
definition.units[1], "name", "Template office"
|
||||
)
|
||||
)
|
||||
office = self.session.query(OrganizationUnit).filter_by(
|
||||
tenant_id="tenant-1", slug="office"
|
||||
).one()
|
||||
office.name = "Local office"
|
||||
self.session.commit()
|
||||
preview = create_model_upgrade_preview(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
target_version=target,
|
||||
actor_account_id="account-1",
|
||||
idempotency_key="preview-explicit-decision",
|
||||
)
|
||||
self.session.commit()
|
||||
applied, _instantiation = apply_model_upgrade(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
upgrade_id=preview.id,
|
||||
expected_revision=1,
|
||||
decisions={"units:office": {"action": "use_target"}},
|
||||
actor_account_id="account-2",
|
||||
)
|
||||
self.session.commit()
|
||||
self.assertEqual("applied", applied.status)
|
||||
self.assertEqual(
|
||||
"Template office",
|
||||
self.session.query(OrganizationUnit)
|
||||
.filter_by(tenant_id="tenant-1", slug="office")
|
||||
.one()
|
||||
.name,
|
||||
)
|
||||
self.assertEqual(
|
||||
{"action": "use_target"},
|
||||
applied.decisions["units:office"],
|
||||
)
|
||||
|
||||
def test_invalid_local_references_block_and_preview_can_be_cancelled(self) -> None:
|
||||
_template, _source, target = self._instantiated_upgrade_fixture()
|
||||
office = self.session.query(OrganizationUnit).filter_by(
|
||||
tenant_id="tenant-1", slug="office"
|
||||
).one()
|
||||
office.unit_type_id = "missing-unit-type"
|
||||
self.session.commit()
|
||||
preview = create_model_upgrade_preview(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
target_version=target,
|
||||
actor_account_id="account-1",
|
||||
idempotency_key="preview-invalid",
|
||||
)
|
||||
self.session.commit()
|
||||
self.assertGreater(preview.preview["blocking_invalid_references"], 0)
|
||||
with self.assertRaisesRegex(OrganizationUpgradeError, "Invalid tenant references"):
|
||||
apply_model_upgrade(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
upgrade_id=preview.id,
|
||||
expected_revision=1,
|
||||
decisions={},
|
||||
actor_account_id="account-2",
|
||||
)
|
||||
self.session.rollback()
|
||||
cancelled = cancel_model_upgrade(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
upgrade_id=preview.id,
|
||||
expected_revision=1,
|
||||
actor_account_id="account-1",
|
||||
)
|
||||
self.session.commit()
|
||||
self.assertEqual("cancelled", cancelled.status)
|
||||
self.assertEqual(2, cancelled.revision)
|
||||
self.assertIsNone(cancelled.applied_at)
|
||||
|
||||
def test_unchanged_upgrade_preview_has_no_changes(self) -> None:
|
||||
_template, _source, target = self._instantiated_upgrade_fixture()
|
||||
preview = create_model_upgrade_preview(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
target_version=target,
|
||||
actor_account_id="account-1",
|
||||
idempotency_key="preview-unchanged",
|
||||
)
|
||||
self.assertEqual([], preview.preview["entries"])
|
||||
self.assertEqual(0, preview.preview["requires_decisions"])
|
||||
|
||||
def _instantiated_upgrade_fixture(self, mutate_target=None):
|
||||
source_definition = _definition()
|
||||
source_payload, source_hash = canonical_template_definition(source_definition)
|
||||
target_definition = source_definition.model_copy(deep=True)
|
||||
if mutate_target is not None:
|
||||
mutate_target(target_definition)
|
||||
target_payload, target_hash = canonical_template_definition(target_definition)
|
||||
template = OrganizationModelTemplate(
|
||||
id="template-upgrade",
|
||||
slug="upgrade-template",
|
||||
name="Upgrade template",
|
||||
)
|
||||
source_version = OrganizationModelTemplateVersion(
|
||||
id="template-upgrade-v1",
|
||||
template_id=template.id,
|
||||
version="1.0.0",
|
||||
status="published",
|
||||
definition=source_payload,
|
||||
definition_sha256=source_hash,
|
||||
)
|
||||
target_version = OrganizationModelTemplateVersion(
|
||||
id="template-upgrade-v2",
|
||||
template_id=template.id,
|
||||
version="2.0.0",
|
||||
status="published",
|
||||
definition=target_payload,
|
||||
definition_sha256=target_hash,
|
||||
)
|
||||
self.session.add_all([template, source_version, target_version])
|
||||
self.session.flush()
|
||||
source = instantiate_template_version(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
template=template,
|
||||
version=source_version,
|
||||
actor_account_id="account-1",
|
||||
)
|
||||
self.session.commit()
|
||||
return template, source, target_version
|
||||
|
||||
|
||||
def _definition() -> OrganizationModelTemplateDefinition:
|
||||
return OrganizationModelTemplateDefinition.model_validate(
|
||||
|
||||
Reference in New Issue
Block a user