using System.Text.Json; using ZB.MOM.WW.CBDDC.Core.Sync; namespace ZB.MOM.WW.CBDDC.Core.Tests; public class RecursiveNodeMergeConflictResolverTests { private readonly RecursiveNodeMergeConflictResolver _resolver; /// /// Initializes a new instance of the class. /// public RecursiveNodeMergeConflictResolverTests() { _resolver = new RecursiveNodeMergeConflictResolver(); } private Document CreateDoc(string key, object data, HlcTimestamp ts) { var json = JsonSerializer.Serialize(data); var element = JsonDocument.Parse(json).RootElement; return new Document("test", key, element, ts, false); } private OplogEntry CreateOp(string key, object data, HlcTimestamp ts) { var json = JsonSerializer.Serialize(data); var element = JsonDocument.Parse(json).RootElement; return new OplogEntry("test", key, OperationType.Put, element, ts, string.Empty); } /// /// Verifies that disjoint fields are merged into a single document. /// [Fact] public void Resolve_ShouldMergeDisjointFields() { // Arrange var ts1 = new HlcTimestamp(100, 0, "n1"); var ts2 = new HlcTimestamp(200, 0, "n2"); var doc = CreateDoc("k1", new { name = "Alice" }, ts1); var op = CreateOp("k1", new { age = 30 }, ts2); // Act var result = _resolver.Resolve(doc, op); // Assert result.ShouldApply.ShouldBeTrue(); result.MergedDocument.ShouldNotBeNull(); var merged = result.MergedDocument.Content; merged.GetProperty("name").GetString().ShouldBe("Alice"); merged.GetProperty("age").GetInt32().ShouldBe(30); result.MergedDocument.UpdatedAt.ShouldBe(ts2); // Max timestamp } /// /// Verifies that primitive collisions are resolved using the higher timestamp value. /// [Fact] public void Resolve_ShouldPrioritizeHigherTimestamp_PrimitiveCollision() { // Arrange var oldTs = new HlcTimestamp(100, 0, "n1"); var newTs = new HlcTimestamp(200, 0, "n2"); var doc = CreateDoc("k1", new { val = "Old" }, oldTs); var op = CreateOp("k1", new { val = "New" }, newTs); // Act - Remote is newer var result1 = _resolver.Resolve(doc, op); result1.MergedDocument!.Content.GetProperty("val").GetString().ShouldBe("New"); // Act - Local is newer (simulating outdated remote op) var docNew = CreateDoc("k1", new { val = "Correct" }, newTs); var opOld = CreateOp("k1", new { val = "Stale" }, oldTs); var result2 = _resolver.Resolve(docNew, opOld); result2.MergedDocument!.Content.GetProperty("val").GetString().ShouldBe("Correct"); } /// /// Verifies that nested object content is merged recursively. /// [Fact] public void Resolve_ShouldRecursivelyMergeObjects() { // Arrange var ts1 = new HlcTimestamp(100, 0, "n1"); var ts2 = new HlcTimestamp(200, 0, "n2"); var doc = CreateDoc("k1", new { info = new { x = 1, y = 1 } }, ts1); var op = CreateOp("k1", new { info = new { y = 2, z = 3 } }, ts2); // Act var result = _resolver.Resolve(doc, op); // Assert var info = result.MergedDocument!.Content.GetProperty("info"); info.GetProperty("x").GetInt32().ShouldBe(1); info.GetProperty("y").GetInt32().ShouldBe(2); // Overwritten by newer info.GetProperty("z").GetInt32().ShouldBe(3); // Added } /// /// Verifies that arrays containing object identifiers are merged by item identity. /// [Fact] public void Resolve_ShouldMergeArraysById() { // Arrange var ts1 = new HlcTimestamp(100, 0, "n1"); var ts2 = new HlcTimestamp(200, 0, "n2"); var doc = CreateDoc("k1", new { items = new[] { new { id = "1", val = "A" }, new { id = "2", val = "B" } } }, ts1); var op = CreateOp("k1", new { items = new[] { new { id = "1", val = "A-Updated" }, // Update new { id = "3", val = "C" } // Insert } }, ts2); // Act var result = _resolver.Resolve(doc, op); // Assert Action validate = (root) => { var items = root.GetProperty("items"); items.GetArrayLength().ShouldBe(3); // Order is not guaranteed, so find by id // But simplified test checking content exists var text = items.GetRawText(); text.ShouldContain("A-Updated"); text.ShouldContain("B"); text.ShouldContain("C"); }; validate(result.MergedDocument!.Content); } /// /// Verifies that primitive arrays fall back to last-write-wins behavior. /// [Fact] public void Resolve_ShouldFallbackToLWW_ForPrimitiveArrays() { // Arrange var ts1 = new HlcTimestamp(100, 0, "n1"); var ts2 = new HlcTimestamp(200, 0, "n2"); var doc = CreateDoc("k1", new { tags = new[] { "a", "b" } }, ts1); var op = CreateOp("k1", new { tags = new[] { "c" } }, ts2); // Act var result = _resolver.Resolve(doc, op); // Assert var tags = result.MergedDocument!.Content.GetProperty("tags"); tags.GetArrayLength().ShouldBe(1); tags[0].GetString().ShouldBe("c"); } }