fix(r2-02): non-lossy ResilienceFormModel round-trip — preserve unknown keys, never overwrite unparseable stored JSON (04/C-7)

This commit is contained in:
Joseph Doherty
2026-07-13 10:18:31 -04:00
parent 07eec11d31
commit 9e0a05dc6a
3 changed files with 180 additions and 60 deletions
@@ -1,3 +1,4 @@
using System.Text.Json.Nodes;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers;
@@ -6,6 +7,59 @@ using ZB.MOM.WW.OtOpcUa.Core.Resilience;
public class ResilienceFormModelTests
{
// ---- 04/C-7: non-lossy round-trip ----
[Fact]
public void Unknown_top_level_key_survives_round_trip()
{
// bulkheadMaxConcurrent doubles as the 01/U-6 continuity proof: the deleted knob's key survives.
var json = """{"bulkheadMaxConcurrent":16,"capabilityPolicies":{"Read":{"timeoutSeconds":5}}}""";
var back = ResilienceFormModel.FromJson(json).ToJson();
back.ShouldNotBeNull();
var obj = JsonNode.Parse(back)!.AsObject();
obj["bulkheadMaxConcurrent"]!.GetValue<int>().ShouldBe(16);
obj["capabilityPolicies"]!["Read"]!["timeoutSeconds"]!.GetValue<int>().ShouldBe(5);
}
[Fact]
public void Unknown_capability_entry_survives_round_trip()
{
var json = """{"capabilityPolicies":{"Read":{"timeoutSeconds":5},"FutureCap":{"timeoutSeconds":9}}}""";
var back = ResilienceFormModel.FromJson(json).ToJson();
back.ShouldNotBeNull();
var obj = JsonNode.Parse(back)!.AsObject();
obj["capabilityPolicies"]!["FutureCap"]!["timeoutSeconds"]!.GetValue<int>().ShouldBe(9);
obj["capabilityPolicies"]!["Read"]!["timeoutSeconds"]!.GetValue<int>().ShouldBe(5);
}
[Fact]
public void Unknown_per_policy_field_survives_round_trip()
{
var json = """{"capabilityPolicies":{"Read":{"timeoutSeconds":5,"futureField":7}}}""";
var back = ResilienceFormModel.FromJson(json).ToJson();
back.ShouldNotBeNull();
var read = JsonNode.Parse(back)!.AsObject()["capabilityPolicies"]!["Read"]!.AsObject();
read["timeoutSeconds"]!.GetValue<int>().ShouldBe(5);
read["futureField"]!.GetValue<int>().ShouldBe(7);
}
[Fact]
public void Malformed_json_sets_ParseFailed_and_ToJson_returns_original_text()
{
const string json = "{ not json";
var m = ResilienceFormModel.FromJson(json);
m.ParseFailed.ShouldBeTrue();
m.ToJson().ShouldBe(json); // never overwrite what it couldn't read
}
[Fact]
public void Blank_form_serializes_to_null()
=> new ResilienceFormModel().ToJson().ShouldBeNull();