feat: harden shared platform contracts
This commit is contained in:
175
tests/test_optimistic_concurrency.py
Normal file
175
tests/test_optimistic_concurrency.py
Normal file
@@ -0,0 +1,175 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user