diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/CalculationDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/CalculationDriverProbe.cs index 060cf5bd..0a34b959 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/CalculationDriverProbe.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation/CalculationDriverProbe.cs @@ -13,10 +13,16 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Calculation; /// public sealed class CalculationDriverProbe : IDriverProbe { + // Mirrors CalculationDriverFactoryExtensions.JsonOptions / CalculationDriver.ReinitJsonOptions + // (AllowTrailingCommas + ReadCommentHandling included) so a config blob the real spawn/reinit path + // tolerates never fails Test-Connect as "invalid JSON" — the probe must never be STRICTER than the + // path it is standing in for. private static readonly JsonSerializerOptions Opts = new() { PropertyNameCaseInsensitive = true, UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, Converters = { new JsonStringEnumConverter() }, }; diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation.Tests/CalculationDriverProbeTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation.Tests/CalculationDriverProbeTests.cs new file mode 100644 index 00000000..5d1be8f4 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Calculation.Tests/CalculationDriverProbeTests.cs @@ -0,0 +1,72 @@ +using Shouldly; +using Xunit; + +namespace ZB.MOM.WW.OtOpcUa.Driver.Calculation.Tests; + +/// +/// Unit tests for — the Calculation pseudo-driver's Test-Connect +/// probe (parse-only, since there is no backend to reach). +/// +public sealed class CalculationDriverProbeTests +{ + private static readonly CalculationDriverProbe Sut = new(); + + [Fact] + public async Task Empty_config_json_fails() + { + var result = await Sut.ProbeAsync("", TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); + result.Ok.ShouldBeFalse(); + result.Message.ShouldNotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task Malformed_json_fails() + { + var result = await Sut.ProbeAsync("not json", TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); + result.Ok.ShouldBeFalse(); + result.Message.ShouldNotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task A_config_with_no_rawTags_array_is_still_valid() + { + var result = await Sut.ProbeAsync("{}", TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); + result.Ok.ShouldBeTrue(result.Message); + } + + [Fact] + public async Task A_well_formed_rawTags_config_is_valid() + { + const string json = """{"rawTags":[{"rawPath":"calc/e/x","tagConfig":"{}","writeIdempotent":false}]}"""; + var result = await Sut.ProbeAsync(json, TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); + result.Ok.ShouldBeTrue(result.Message); + } + + /// + /// The probe must not be STRICTER than the real spawn/reinit path: both + /// CalculationDriverFactoryExtensions.JsonOptions and CalculationDriver.ReinitJsonOptions + /// set AllowTrailingCommas + ReadCommentHandling.Skip. Before this pin the probe's own + /// options omitted both, so a config a real deploy would accept could fail Test-Connect as "invalid". + /// + [Fact] + public async Task Tolerates_trailing_commas_like_the_real_spawn_and_reinit_paths() + { + const string json = """{"rawTags":[{"rawPath":"calc/e/x","tagConfig":"{}","writeIdempotent":false},],}"""; + var result = await Sut.ProbeAsync(json, TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); + result.Ok.ShouldBeTrue(result.Message); + } + + /// Same parity claim for comments, which the spawn/reinit path also tolerates. + [Fact] + public async Task Tolerates_comments_like_the_real_spawn_and_reinit_paths() + { + const string json = """ + { + // a hand-edited config comment + "rawTags": [] + } + """; + var result = await Sut.ProbeAsync(json, TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); + result.Ok.ShouldBeTrue(result.Message); + } +}