Files
lmxopcua/tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/DraftValidatorSqlQueryTests.cs
T
Joseph Doherty 241318e7e5
v2-ci / unit-tests (push) Has been cancelled
feat(deploy): reject Sql Query tags naming undeclared queries; warn on Writable/writable disagreement (lmxopcua#781)
Item 5: DraftValidator now emits SqlQueryNotDeclared (always blocking) for a Query tag whose
query is absent from the merged driver config's queries map, via the driver's own tag parse.
Item 3: StartDeployment appends a non-blocking warning to the accepted message when the Tag
entity's AccessLevel and the Sql blob's writable flag disagree.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 18:08:54 -05:00

77 lines
3.5 KiB
C#

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;
/// <summary>lmxopcua#781 items 5 and 3: Sql deploy checks that need the driver config / Tag entity.</summary>
[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();
}