using System.Text.Json.Nodes; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; 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().ShouldBe(16); obj["capabilityPolicies"]!["Read"]!["timeoutSeconds"]!.GetValue().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().ShouldBe(9); obj["capabilityPolicies"]!["Read"]!["timeoutSeconds"]!.GetValue().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().ShouldBe(5); read["futureField"]!.GetValue().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(); [Fact] public void Partial_override_round_trips() { var m = new ResilienceFormModel { RecycleIntervalSeconds = 3600 }; m.Policies["Read"].TimeoutSeconds = 5; m.Policies["Read"].RetryCount = 5; var json = m.ToJson(); json.ShouldNotBeNull(); var back = ResilienceFormModel.FromJson(json); back.RecycleIntervalSeconds.ShouldBe(3600); back.Policies["Read"].TimeoutSeconds.ShouldBe(5); back.Policies["Write"].IsEmpty.ShouldBeTrue(); } [Fact] public void Malformed_json_yields_empty_model() { var m = ResilienceFormModel.FromJson("{ not valid json"); m.RecycleIntervalSeconds.ShouldBeNull(); m.Policies["Read"].IsEmpty.ShouldBeTrue(); } [Fact] public void Validate_flags_out_of_range_values_with_capability_and_range() { var m = new ResilienceFormModel(); m.Policies["Read"].TimeoutSeconds = 0; // < MinTimeoutSeconds m.Policies["Read"].RetryCount = -1; // < MinRetryCount m.Policies["Read"].BreakerFailureThreshold = 1; // below Polly MinimumThroughput floor var msgs = m.Validate(); msgs.Count.ShouldBe(3); msgs.ShouldContain(s => s.Contains("Read") && s.Contains("timeout")); msgs.ShouldContain(s => s.Contains("Read") && s.Contains("retries")); msgs.ShouldContain(s => s.Contains("Read") && s.Contains("breaker")); } [Fact] public void Validate_passes_in_range_and_blank() { var blank = new ResilienceFormModel(); blank.Validate().ShouldBeEmpty(); var inRange = new ResilienceFormModel(); inRange.Policies["Read"].TimeoutSeconds = 5; inRange.Policies["Read"].RetryCount = 3; inRange.Policies["Read"].BreakerFailureThreshold = 4; inRange.Policies["Write"].BreakerFailureThreshold = 0; // 0 = breaker off, legal inRange.Validate().ShouldBeEmpty(); } [Fact] public void Emitted_json_is_consumable_by_the_runtime_parser() { var m = new ResilienceFormModel(); m.Policies["Read"].TimeoutSeconds = 7; var opts = DriverResilienceOptionsParser.ParseOrDefaults(DriverTier.B, m.ToJson(), out var diag); diag.ShouldBeNull(); opts.Resolve(DriverCapability.Read).TimeoutSeconds.ShouldBe(7); opts.Resolve(DriverCapability.Write).RetryCount.ShouldBe(0); } }