Files
CBDDC/tests/ZB.MOM.WW.CBDDC.Core.Tests/RecursiveNodeMergeConflictResolverTests.cs

174 lines
5.8 KiB
C#
Executable File

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;
/// <summary>
/// Initializes a new instance of the <see cref="RecursiveNodeMergeConflictResolverTests"/> class.
/// </summary>
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);
}
/// <summary>
/// Verifies that disjoint fields are merged into a single document.
/// </summary>
[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
}
/// <summary>
/// Verifies that primitive collisions are resolved using the higher timestamp value.
/// </summary>
[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");
}
/// <summary>
/// Verifies that nested object content is merged recursively.
/// </summary>
[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
}
/// <summary>
/// Verifies that arrays containing object identifiers are merged by item identity.
/// </summary>
[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<JsonElement> 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);
}
/// <summary>
/// Verifies that primitive arrays fall back to last-write-wins behavior.
/// </summary>
[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");
}
}