174 lines
5.8 KiB
C#
Executable File
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");
|
|
}
|
|
}
|