Files
govoplan-core/tests/test_optimistic_concurrency.py

176 lines
6.2 KiB
Python

from __future__ import annotations
import unittest
from sqlalchemy import Integer, String, create_engine
from sqlalchemy.orm import Mapped, Session, mapped_column, sessionmaker
from sqlalchemy.pool import StaticPool
from govoplan_core.core.concurrency import (
MissingPreconditionError,
RevisionConflictError,
assert_revision_precondition,
claim_revision,
if_match_matches,
strong_resource_etag,
three_way_merge,
)
from govoplan_core.db.base import Base
class _RevisionFixture(Base):
__tablename__ = "test_revision_fixture"
id: Mapped[str] = mapped_column(String(36), primary_key=True)
edit_revision: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
value: Mapped[str] = mapped_column(String(100), nullable=False)
class OptimisticConcurrencyTests(unittest.TestCase):
def test_strong_etags_and_if_match_use_strong_comparison(self) -> None:
etag = strong_resource_etag("campaign_version", "version-1", 3)
self.assertTrue(if_match_matches(etag, etag))
self.assertTrue(if_match_matches(f'"other", {etag}', etag))
self.assertFalse(if_match_matches(f"W/{etag}", etag))
self.assertNotEqual(
etag,
strong_resource_etag("campaign_version", "version-1", 4),
)
with self.assertRaises(MissingPreconditionError):
assert_revision_precondition(
None,
resource_type="campaign_version",
resource_id="version-1",
submitted_base_revision=3,
)
def test_compare_and_set_allows_only_one_writer_for_a_revision(self) -> None:
engine = create_engine(
"sqlite+pysqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
_RevisionFixture.__table__.create(engine)
session_factory = sessionmaker(bind=engine, class_=Session)
with session_factory() as session:
session.add(_RevisionFixture(id="resource-1", value="base"))
session.commit()
next_revision = claim_revision(
session,
model=_RevisionFixture,
filters=(_RevisionFixture.id == "resource-1",),
revision_attribute="edit_revision",
expected_revision=1,
resource_type="fixture",
resource_id="resource-1",
)
session.commit()
self.assertEqual(next_revision, 2)
with self.assertRaises(RevisionConflictError) as raised:
claim_revision(
session,
model=_RevisionFixture,
filters=(_RevisionFixture.id == "resource-1",),
revision_attribute="edit_revision",
expected_revision=1,
resource_type="fixture",
resource_id="resource-1",
)
self.assertEqual(raised.exception.current_revision, 2)
engine.dispose()
def test_disjoint_map_and_stable_id_collection_changes_merge(self) -> None:
base = {
"campaign": {"name": "Original", "description": "Base"},
"entries": {
"inline": [
{"id": "one", "name": "One", "active": True},
{"id": "two", "name": "Two", "active": True},
]
},
}
local = {
**base,
"campaign": {"name": "Local", "description": "Base"},
"entries": {
"inline": [
{"id": "one", "name": "One", "active": False},
{"id": "two", "name": "Two", "active": True},
]
},
}
current = {
**base,
"campaign": {"name": "Original", "description": "Remote"},
"entries": {
"inline": [
{"id": "one", "name": "One", "active": True},
{"id": "two", "name": "Remote Two", "active": True},
]
},
}
result = three_way_merge(base, local, current)
self.assertTrue(result.merged)
self.assertEqual(result.value["campaign"], {"name": "Local", "description": "Remote"})
self.assertEqual(result.value["entries"]["inline"][0]["active"], False)
self.assertEqual(result.value["entries"]["inline"][1]["name"], "Remote Two")
def test_same_path_unkeyed_collection_and_protected_edits_conflict(self) -> None:
same_path = three_way_merge(
{"name": "Base"},
{"name": "Local"},
{"name": "Remote"},
)
self.assertEqual(same_path.conflicts[0].kind, "same_path_changed")
unkeyed = three_way_merge(
{"values": ["a"]},
{"values": ["a", "local"]},
{"values": ["a", "remote"]},
)
self.assertEqual(unkeyed.conflicts[0].kind, "unkeyed_collection")
protected = three_way_merge(
{"workflow_state": "draft", "name": "Base"},
{"workflow_state": "ready", "name": "Base"},
{"workflow_state": "draft", "name": "Remote"},
protected_paths=("/workflow_state",),
)
self.assertEqual(protected.conflicts[0].kind, "protected_path")
def test_delete_versus_edit_and_conflicting_reorder_remain_explicit(self) -> None:
base = [{"id": "one", "value": "1"}, {"id": "two", "value": "2"}]
delete_vs_edit = three_way_merge(
base,
[{"id": "two", "value": "2"}],
[{"id": "one", "value": "remote"}, {"id": "two", "value": "2"}],
)
self.assertEqual(delete_vs_edit.conflicts[0].kind, "delete_vs_edit")
reordered = three_way_merge(
[
{"id": "one"},
{"id": "two"},
{"id": "three"},
],
[
{"id": "two"},
{"id": "one"},
{"id": "three"},
],
[
{"id": "one"},
{"id": "three"},
{"id": "two"},
],
)
self.assertEqual(reordered.conflicts[0].kind, "collection_reorder")
if __name__ == "__main__":
unittest.main()