using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
///
/// The deploy gate for lmxopcua#835 (a credential reference naming ANOTHER driver instance's secret) and #836 (the
/// OpcUaClient user-certificate path confined to the node's certificate directory).
///
[Trait("Category", "Unit")]
public sealed class DraftValidatorCredentialInstanceAndPathTests
{
private static IReadOnlyList Validate(params DriverInstance[] drivers) =>
Validate(drivers, []);
private static IReadOnlyList Validate(DriverInstance[] drivers, Device[] devices) =>
DraftValidator.Validate(new DraftSnapshot
{
GenerationId = 1,
ClusterId = "c",
DriverInstances = drivers,
Devices = devices,
Tags = [],
}).Where(e => e.Code is CredentialReferenceDeployChecks.Code or CredentialReferenceDeployChecks.OtherInstanceCode).ToList();
private static DriverInstance Drv(string id, string name, string type, string config) =>
new() { DriverInstanceId = id, ClusterId = "c", Name = name, DriverType = type, DriverConfig = config };
[Fact]
public void A_galaxy_driver_naming_another_instances_key_is_refused()
{
var e = Validate(
Drv("DRV-victim", "Victim", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/DRV-victim/apikey"}}"""),
Drv("DRV-evil", "Evil", "GalaxyMxGateway", """{"Gateway":{"Endpoint":"http://evil:5001","ApiKeySecretRef":"secret:galaxy/DRV-victim/apikey"}}"""))
.ShouldHaveSingleItem();
e.Code.ShouldBe(CredentialReferenceDeployChecks.OtherInstanceCode);
e.Context.ShouldBe("DRV-evil");
e.Message.ShouldContain("lmxopcua#835");
}
[Fact]
public void A_device_config_naming_another_instance_by_name_is_refused() =>
Validate(
[
Drv("DRV-a", "Victim", "OpcUaClient", "{}"),
Drv("DRV-b", "Evil", "OpcUaClient", "{}"),
],
[new Device { DeviceId = "dev-b", DriverInstanceId = "DRV-b", Name = "d", DeviceConfig = """{"Password":"secret:opcuaclient/victim/pw"}""" }])
.ShouldHaveSingleItem().Context.ShouldBe("dev-b");
/// lmxopcua#849: the env: form carries the same per-instance rule, under the same code.
[Fact]
public void A_galaxy_env_reference_naming_another_instance_is_refused()
{
var e = Validate(
Drv("DRV-victim", "Victim", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"env:OTOPCUA_GALAXY_VICTIM_KEY"}}"""),
Drv("DRV-evil", "Evil", "GalaxyMxGateway", """{"Gateway":{"Endpoint":"http://evil:5001","ApiKeySecretRef":"env:GALAXY_MXGW_DRV_VICTIM_KEY"}}"""))
.ShouldHaveSingleItem();
e.Code.ShouldBe(CredentialReferenceDeployChecks.OtherInstanceCode);
e.Context.ShouldBe("DRV-evil");
e.Message.ShouldContain("lmxopcua#849");
}
[Fact]
public void Shared_and_own_env_variables_pass() =>
Validate(
Drv("DRV-a", "A", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"env:OTOPCUA_GALAXY_DRV_A_KEY"}}"""),
Drv("DRV-b", "B", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"env:GALAXY_MXGW_API_KEY"}}"""),
Drv("DRV-c", "C", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"env:GALAXY_MXGW_API_KEY"}}"""))
.ShouldBeEmpty();
/// #849 review: a pre-existing driver named 'drv' must not fail the id-form variables of its siblings.
[Fact]
public void A_driver_named_drv_does_not_break_id_form_env_variables() =>
Validate(
Drv("DRV-0123456789ab", "A", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"env:OTOPCUA_GALAXY_DRV_0123456789AB_KEY"}}"""),
Drv("DRV-fedcba987654", "drv", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"env:OTOPCUA_GALAXY_DRV_FEDCBA987654_KEY"}}"""))
.ShouldBeEmpty();
[Fact]
public void Shared_and_own_keys_pass() =>
Validate(
Drv("DRV-a", "A", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/DRV-a/apikey"}}"""),
Drv("DRV-b", "B", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/gw/apikey"}}"""),
Drv("DRV-c", "C", "GalaxyMxGateway", """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/gw/apikey"}}"""))
.ShouldBeEmpty();
[Fact]
public void Heidenhain_cross_instance_is_reported_by_this_gate() =>
DraftValidator.Validate(new DraftSnapshot
{
GenerationId = 1, ClusterId = "c",
DriverInstances =
[
Drv("H1", "line1", "Heidenhain", """{"Access":{"InspectPassword":"secret:heidenhain/line1/inspect"}}"""),
Drv("H2", "line2", "Heidenhain", """{"Access":{"InspectPassword":"secret:heidenhain/line1/inspect"}}"""),
],
Devices = [], Tags = [],
})
.Where(e => e.Code == CredentialReferenceDeployChecks.OtherInstanceCode)
.ShouldHaveSingleItem().Context.ShouldBe("H2");
[Theory]
[InlineData("""{"AuthType":"Certificate","UserCertificatePath":"C:\\Windows\\System32\\config\\systemprofile\\host.pfx"}""")]
[InlineData("""{"UserCertificatePath":"/etc/ssl/private/host.pfx"}""")]
[InlineData("""{"UserCertificatePath":"plant1/../../host.pfx"}""")]
public void An_opcuaclient_user_certificate_path_outside_the_directory_is_refused(string config)
{
var e = Validate(Drv("DRV-o", "o", "OpcUaClient", config)).ShouldHaveSingleItem();
e.Code.ShouldBe(CredentialReferenceDeployChecks.Code);
e.Message.ShouldContain("lmxopcua#836");
e.Message.ShouldNotContain("host.pfx");
}
/// Review L-1: a path in an object that explicitly switches certificate auth off is not checked…
[Fact]
public void A_path_under_explicit_non_certificate_auth_passes() =>
Validate(Drv("DRV-o", "o", "OpcUaClient", """{"AuthType":"Anonymous","UserCertificatePath":"C:\\stale\\host.pfx"}""")).ShouldBeEmpty();
/// …but the driver/device split cannot smuggle one past the gate: the runtime-merged config is checked too.
[Fact]
public void A_path_split_from_its_certificate_switch_is_caught_on_the_merged_config()
{
var e = Validate(
[Drv("DRV-o", "o", "OpcUaClient", """{"AuthType":"Anonymous","UserCertificatePath":"C:\\stolen\\host.pfx"}""")],
[new Device { DeviceId = "dev-o", DriverInstanceId = "DRV-o", Name = "d", DeviceConfig = """{"AuthType":"Certificate"}""" }])
.ShouldHaveSingleItem();
e.Context.ShouldBe("DRV-o");
e.Message.ShouldContain("merged with its devices");
e.Message.ShouldContain("lmxopcua#836");
}
[Fact]
public void A_relative_user_certificate_path_passes() =>
Validate(Drv("DRV-o", "o", "OpcUaClient", """{"AuthType":"Certificate","UserCertificatePath":"plant1/user.pfx"}""")).ShouldBeEmpty();
}