54f58c583c
- Review 1: RawTreeService Create/UpdateDriver and Create/UpdateDevice refuse a Heidenhain config whose password fields are not blank / secret:heidenhain/... / env:OTOPCUA_HEIDENHAIN_... (DriverConfigSaveGate over HeidenhainOptionsParser.DescribeCredentialProblems: top level, Access, Devices[], case-insensitive keys; the value is never echoed), so a literal password never reaches the draft DB. The driver and device modals show the reason and disable Save. - Review 8: CreateTag/UpdateTag/ImportTags (tag modal, manual entry, CSV import, browse commit) refuse a ReadWrite Heidenhain tag or a Tag.DataType that disagrees with what the driver publishes (HeidenhainTagRules: catalog type, Float64 => Double, or the binding's dataType). The tag modal and manual-entry grid fix the access level at Read and derive the data type from the address (RawTagRowDefaults). New deploy gate HeidenhainTagDataTypeMismatch. - docs/drivers/Heidenhain.md: credential namespace convention + why, the save-path and deploy enforcement, optional-role credential behaviour, BadConfigurationError, backoff/ordering notes. Tests: HeidenhainSavePathTests (service save path; red with the gates disabled) and new DraftValidatorHeidenhainTests cases.
100 lines
4.6 KiB
C#
100 lines
4.6 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#803: the Heidenhain read-only, credential and endpoint deploy gates.</summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class DraftValidatorHeidenhainTests
|
|
{
|
|
private static DriverInstance Driver(string config = "{}", string type = "Heidenhain") => new()
|
|
{
|
|
DriverInstanceId = "hh", ClusterId = "c", Name = "hh", DriverType = type, DriverConfig = config,
|
|
};
|
|
|
|
private static Device Dev(string config = """{"Host":"10.0.0.5"}""") => new()
|
|
{
|
|
DeviceId = "d1", DriverInstanceId = "hh", Name = "dev1", DeviceConfig = config,
|
|
};
|
|
|
|
private static Tag T(TagAccessLevel access) => new()
|
|
{
|
|
TagId = "t1", DeviceId = "d1", Name = "ExecState", DataType = "Int32", AccessLevel = access,
|
|
TagConfig = """{"address":"Status/ExecutionState"}""",
|
|
};
|
|
|
|
private static IReadOnlyList<ValidationError> Validate(DriverInstance d, Device dev, params Tag[] tags) =>
|
|
DraftValidator.Validate(new DraftSnapshot
|
|
{
|
|
GenerationId = 1, ClusterId = "c", DriverInstances = [d], Devices = [dev], Tags = tags,
|
|
});
|
|
|
|
[Fact]
|
|
public void A_read_only_tag_on_a_configured_control_is_valid()
|
|
=> Validate(Driver(), Dev(), T(TagAccessLevel.Read)).ShouldNotContain(e => e.Code.StartsWith("Heidenhain", StringComparison.Ordinal));
|
|
|
|
[Fact]
|
|
public void A_ReadWrite_tag_is_refused_so_no_node_is_ever_writable()
|
|
{
|
|
var e = Validate(Driver(), Dev(), T(TagAccessLevel.ReadWrite)).Single(x => x.Code == HeidenhainDeployChecks.WritableCode);
|
|
e.Context.ShouldBe("t1");
|
|
e.Message.ShouldContain("read-only");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("""{"Access":{"InspectPassword":"hunter2"}}""", "{\"Host\":\"10.0.0.5\"}")]
|
|
[InlineData("{}", """{"Host":"10.0.0.5","DncPassword":"hunter2"}""")]
|
|
public void A_literal_password_is_refused_without_echoing_it(string driverConfig, string deviceConfig)
|
|
{
|
|
var errors = Validate(Driver(driverConfig), Dev(deviceConfig), T(TagAccessLevel.Read));
|
|
errors.ShouldContain(e => e.Code == HeidenhainDeployChecks.CredentialCode);
|
|
errors.ShouldAllBe(e => !e.Message.Contains("hunter2"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Secret_and_env_references_are_accepted()
|
|
=> Validate(Driver("""{"Access":{"InspectPassword":"secret:heidenhain/inspect","DncPassword":"env:OTOPCUA_HEIDENHAIN_DNC"}}"""), Dev(), T(TagAccessLevel.Read))
|
|
.ShouldNotContain(e => e.Code.StartsWith("Heidenhain", StringComparison.Ordinal));
|
|
|
|
[Fact]
|
|
public void A_device_with_tags_but_no_endpoint_is_refused_while_an_empty_device_is_not()
|
|
{
|
|
Validate(Driver(), Dev("{}"), T(TagAccessLevel.Read)).ShouldContain(e => e.Code == HeidenhainDeployChecks.ConfigCode && e.Context == "d1");
|
|
Validate(Driver(), Dev("{}")).ShouldNotContain(e => e.Code.StartsWith("Heidenhain", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_tag_data_type_that_disagrees_with_the_published_type_is_refused()
|
|
{
|
|
var tag = T(TagAccessLevel.Read);
|
|
tag.DataType = "Float";
|
|
var e = Validate(Driver(), Dev(), tag).Single(x => x.Code == HeidenhainDeployChecks.DataTypeCode);
|
|
e.Context.ShouldBe("t1");
|
|
e.Message.ShouldContain("Int32");
|
|
var binding = T(TagAccessLevel.Read);
|
|
binding.TagConfig = """{"address":"PLC:DWORD:0","dataType":"Int64"}""";
|
|
binding.DataType = "Int64";
|
|
Validate(Driver(), Dev(), binding).ShouldNotContain(x => x.Code == HeidenhainDeployChecks.DataTypeCode);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("env:ConnectionStrings__ConfigDb")]
|
|
[InlineData("secret:galaxy/inst1/apikey")]
|
|
public void An_out_of_namespace_reference_is_refused(string reference)
|
|
=> Validate(Driver("{\"Access\":{\"InspectPassword\":\"" + reference + "\"}}"), Dev(), T(TagAccessLevel.Read))
|
|
.ShouldContain(e => e.Code == HeidenhainDeployChecks.CredentialCode);
|
|
|
|
[Fact]
|
|
public void Out_of_range_settings_are_refused()
|
|
=> Validate(Driver("""{"Polling":{"MaxAlarms":999}}"""), Dev(), T(TagAccessLevel.Read))
|
|
.ShouldContain(e => e.Code == HeidenhainDeployChecks.ConfigCode);
|
|
|
|
[Fact]
|
|
public void Other_drivers_are_unaffected()
|
|
=> Validate(Driver("""{"Access":{"InspectPassword":"x"}}""", type: "Modbus"), Dev(), T(TagAccessLevel.ReadWrite))
|
|
.ShouldNotContain(e => e.Code.StartsWith("Heidenhain", StringComparison.Ordinal));
|
|
}
|