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#781 items 5 and 3: Sql deploy checks that need the driver config / Tag entity. [Trait("Category", "Unit")] public sealed class DraftValidatorSqlQueryTests { private const string Code = SqlTagDeployChecks.UndeclaredQueryCode; private const string Declared = """{"queries":{"line":{"sql":"SELECT 1 AS v","keyColumns":[]}}}"""; private static string QueryTag(string query, bool? writable = null) => $$"""{"model":"Query","query":"{{query}}","column":"v"{{(writable is null ? "" : $",\"writable\":{writable.ToString()!.ToLowerInvariant()}")}}}"""; private static DraftSnapshot Draft(string driverConfig, string tagConfig, TagAccessLevel access = TagAccessLevel.Read, string deviceConfig = "{}") => new() { GenerationId = 1, ClusterId = "c", DriverInstances = [new DriverInstance { DriverInstanceId = "di", ClusterId = "c", Name = "sql", DriverType = "Sql", DriverConfig = driverConfig }], Devices = [new Device { DeviceId = "dev", DriverInstanceId = "di", Name = "d", DeviceConfig = deviceConfig }], Tags = [new Tag { TagId = "t1", DeviceId = "dev", Name = "t", DataType = "Int32", AccessLevel = access, TagConfig = tagConfig }], }; [Fact] public void Query_tag_naming_an_undeclared_query_is_a_deploy_error() { var errors = DraftValidator.Validate(Draft(Declared, QueryTag("nope"))); errors.ShouldContain(e => e.Code == Code && e.Context == "t1" && e.Message.Contains("'nope'")); } [Fact] public void Query_tag_with_no_queries_map_at_all_is_a_deploy_error() => DraftValidator.Validate(Draft("{}", QueryTag("line"))).ShouldContain(e => e.Code == Code); [Fact] public void Positive_control_declared_query_is_quiet() => DraftValidator.Validate(Draft(Declared, QueryTag("line"))).ShouldNotContain(e => e.Code == Code); [Fact] public void Query_declared_on_the_single_device_config_counts_as_declared() => DraftValidator.Validate(Draft("{}", QueryTag("line"), deviceConfig: Declared)) .ShouldNotContain(e => e.Code == Code); [Fact] public void Query_names_are_ordinal() => DraftValidator.Validate(Draft(Declared, QueryTag("LINE"))).ShouldContain(e => e.Code == Code); [Fact] public void Non_query_models_are_not_checked() => DraftValidator.Validate(Draft("{}", """{"model":"KeyValue","table":"t","keyColumn":"k","keyValue":"a","valueColumn":"v"}""")) .ShouldNotContain(e => e.Code == Code); [Theory] [InlineData(TagAccessLevel.ReadWrite, false, true)] [InlineData(TagAccessLevel.Read, true, true)] [InlineData(TagAccessLevel.ReadWrite, true, false)] [InlineData(TagAccessLevel.Read, false, false)] public void Writable_flag_disagreement_warns_and_agreement_is_quiet(TagAccessLevel access, bool writable, bool warns) { var warnings = SqlTagDeployChecks.WritableMismatchWarnings(Draft(Declared, QueryTag("line", writable), access)); (warnings.Count == 1).ShouldBe(warns); if (warns) warnings[0].ShouldContain("t1"); } [Fact] public void Writable_mismatch_is_never_a_validation_error() => DraftValidator.Validate(Draft(Declared, QueryTag("line", true), TagAccessLevel.Read)).ShouldBeEmpty(); }