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()