From 2d218aee033b86f68d4943e94db60895c8ad1ea6 Mon Sep 17 00:00:00 2001 From: konstantin Date: Sun, 14 Jul 2024 17:16:41 +0200 Subject: [PATCH] Reproduce `ArgumentOutOfRangeException` when entity doesn't match patches (#87) --- .../ListPatchingTests.cs | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 ChronoJsonDiffPatch/ChronoJsonDiffPatchTests/ListPatchingTests.cs diff --git a/ChronoJsonDiffPatch/ChronoJsonDiffPatchTests/ListPatchingTests.cs b/ChronoJsonDiffPatch/ChronoJsonDiffPatchTests/ListPatchingTests.cs new file mode 100644 index 0000000..eb9f2ee --- /dev/null +++ b/ChronoJsonDiffPatch/ChronoJsonDiffPatchTests/ListPatchingTests.cs @@ -0,0 +1,130 @@ +using System.Text.Json.Serialization; +using ChronoJsonDiffPatch; +using FluentAssertions; + +namespace ChronoJsonDiffPatchTests; + +public class ListPatchingTests +{ + internal record ListItem + { + [JsonPropertyName("value")] public string Value { get; set; } + } + + internal record EntityWithList + { + [JsonPropertyName("myList")] public List MyList { get; set; } + } + + + [Fact] + public void Test_List_Patching_Generally_Works_With_Add_And_Reverse() + { + var chain = new TimeRangePatchChain(); + var initialEntity = new EntityWithList + { + MyList = new List + { + new() { Value = "Foo" }, + new() { Value = "Bar" } + } + }; + { + var updatedEntity1 = new EntityWithList + { + MyList = new List + { + new() { Value = "fOO" }, + new() { Value = "bAR" } + } + }; + var keyDate1 = new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero); + chain.Add(initialEntity, updatedEntity1, keyDate1); + + chain.Count.Should().Be(2); // [-infinity, keyDate1); [keyDate1, +infinity) + ReverseAndRevert(chain, initialEntity); + } + + { + var updatedEntity2 = new EntityWithList + { + MyList = new List + { + new() { Value = "fOO" }, + new() { Value = "bAR" }, + new() { Value = "bAZ" } + } + }; + var keyDate2 = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero); + chain.Add(initialEntity, updatedEntity2, keyDate2); + + chain.Count.Should().Be(3); // [-infinity, keyDate1); [keyDate1, keyDate2); [keyDate2, +infinity) + ReverseAndRevert(chain, initialEntity); + } + + { + var updatedEntity3 = new EntityWithList + { + MyList = new List + { + new() { Value = "Not so foo anymore" }, + new() { Value = "bAR" }, + new() { Value = "bAZ" } + } + }; + var keyDate3 = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero); + chain.Add(initialEntity, updatedEntity3, keyDate3); + + ReverseAndRevert(chain, initialEntity); + } + } + + /// + /// In we showed that adding and removing list entries is generally well-supported by this library. + /// In this test, we show, than when users run into an , this is probably due to initial entities not matching the expected state (corrupted). + /// + [Fact] + public void Reproduce_ArgumentOutOfRangeException() + { + var chain = new TimeRangePatchChain(); + var initialEntity = new EntityWithList + { + MyList = new List + { + new() { Value = "My First Value" }, + } + }; + var keyDate1 = new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero); + { + var updatedEntity1 = new EntityWithList + { + MyList = new List + { + new() { Value = "My First Value" }, + new() { Value = "My Second Value" } + } + }; + + chain.Add(initialEntity, updatedEntity1, keyDate1); + chain.Count.Should().Be(2); // [-infinity, keyDate1); [keyDate1, +infinity) + ReverseAndRevert(chain, initialEntity); + } + (var antiparallelInitialEntity, var antiparallelChain) = chain.Reverse(initialEntity); + antiparallelInitialEntity.Should().Match(x => x.MyList.Count == 2, because: "Initially the list had 2 items"); + var patchingACorrectInitialEntity = () => antiparallelChain.PatchToDate(antiparallelInitialEntity, keyDate1 - TimeSpan.FromDays(10)); + patchingACorrectInitialEntity.Should().NotThrow(); + + var corruptedInitialEntity = antiparallelInitialEntity; // we modify the reference here, but that's fine. We improve the readability but don't re-use the antiparallelInitialEntity anywhere downstream. + corruptedInitialEntity.MyList.RemoveAt(1); + var applyingPatchesToACorruptedInitialEntity = () => antiparallelChain.PatchToDate(corruptedInitialEntity, keyDate1 - TimeSpan.FromDays(10)); + applyingPatchesToACorruptedInitialEntity.Should().ThrowExactly(); + } + + private static void ReverseAndRevert(TimeRangePatchChain chain, EntityWithList initialEntity) + { + var (reverseEntity, reverseChain) = chain.Reverse(initialEntity); + var (rereverseEntity, rereverseChain) = reverseChain.Reverse(reverseEntity); + rereverseChain.Should().BeEquivalentTo(chain); + initialEntity.Should().BeEquivalentTo(rereverseEntity); + } +}