using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.Configuration.Validation; namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests; /// /// lmxopcua#899: historianReadOnly must be a JSON boolean. The runtime honours only a bool true, so a /// string "true" would deploy as an OWNED tag whose foreign historian tag then gets provisioned (upserted) /// and recorded into — the deploy gate refuses it instead. /// [Trait("Category", "Unit")] public sealed class DraftValidatorHistorianReadOnlyTests { private static DraftSnapshot Draft(params Tag[] tags) => new() { GenerationId = 1, ClusterId = "c", DriverInstances = [ new DriverInstance { DriverInstanceId = "di", ClusterId = "c", Name = "mb", DriverType = "Modbus", DriverConfig = "{}", }, ], Devices = [new Device { DeviceId = "dev", DriverInstanceId = "di", Name = "plc", DeviceConfig = "{}" }], Tags = tags, }; private static Tag TagRow(string id, string tagConfig) => new() { TagId = id, DeviceId = "dev", Name = id, DataType = "Int32", AccessLevel = TagAccessLevel.Read, TagConfig = tagConfig, }; [Theory] [InlineData("{\"isHistorized\":true,\"historianReadOnly\":\"true\"}")] [InlineData("{\"isHistorized\":true,\"historianReadOnly\":1}")] [InlineData("{\"historianReadOnly\":null}")] public void A_non_boolean_historianReadOnly_is_a_deploy_error(string tagConfig) { var errors = DraftValidator.Validate(Draft(TagRow("t-bad", tagConfig))); errors.ShouldContain(e => e.Code == "HistorianReadOnlyInvalid" && e.Context == "t-bad"); } [Theory] [InlineData("{\"isHistorized\":true,\"historianTagname\":\"SysTimeSec\",\"historianReadOnly\":true}")] [InlineData("{\"isHistorized\":true,\"historianReadOnly\":false}")] [InlineData("{\"isHistorized\":true}")] public void A_boolean_or_absent_historianReadOnly_passes(string tagConfig) { var errors = DraftValidator.Validate(Draft(TagRow("t-ok", tagConfig))); errors.ShouldNotContain(e => e.Code == "HistorianReadOnlyInvalid"); } }