using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
/// lmxopcua#815 — the shared credential-reference parser, resolver and per-driver policy table.
public sealed class CredentialReferencesTests
{
private static readonly CredentialScope Galaxy = DriverCredentialPolicy.GalaxyScope;
[Theory]
[InlineData(null, CredentialReferenceKind.Blank)]
[InlineData("", CredentialReferenceKind.Blank)]
[InlineData("secret:galaxy/inst/apikey", CredentialReferenceKind.Secret)]
[InlineData("${secret:galaxy/inst/apikey}", CredentialReferenceKind.Secret)]
[InlineData("secret:otopcua/galaxy/ack-fallback", CredentialReferenceKind.Secret)]
[InlineData("env:OTOPCUA_GALAXY_API_KEY", CredentialReferenceKind.Environment)]
[InlineData("env:GALAXY_MXGW_API_KEY", CredentialReferenceKind.Environment)]
[InlineData("plain-key", CredentialReferenceKind.Literal)]
[InlineData("dev:k", CredentialReferenceKind.Literal)]
[InlineData("secret:otopcua/historian/api-key", CredentialReferenceKind.OutOfScope)]
[InlineData("secret:otopcua/apikeys/ab/galaxy-ack-password", CredentialReferenceKind.OutOfScope)]
[InlineData("secret:OT-MESH-PSK-MAIN", CredentialReferenceKind.OutOfScope)]
[InlineData("secret:galaxy/", CredentialReferenceKind.OutOfScope)]
[InlineData("secret:galaxy/../otopcua/historian/api-key", CredentialReferenceKind.OutOfScope)]
[InlineData("secret:galaxy//x", CredentialReferenceKind.OutOfScope)]
[InlineData("secret:galaxyx/y", CredentialReferenceKind.OutOfScope)]
[InlineData("${secret:galaxy/x", CredentialReferenceKind.OutOfScope)]
[InlineData("SECRET:galaxy/x", CredentialReferenceKind.OutOfScope)]
[InlineData("Env:OTOPCUA_GALAXY_X", CredentialReferenceKind.OutOfScope)]
[InlineData("env:ConnectionStrings__ConfigDb", CredentialReferenceKind.OutOfScope)]
[InlineData("env:ServerHistorian__ApiKey", CredentialReferenceKind.OutOfScope)]
[InlineData("env:OTOPCUA_GALAXY_", CredentialReferenceKind.OutOfScope)]
[InlineData("env:OTOPCUA_GALAXY_lower", CredentialReferenceKind.OutOfScope)]
[InlineData("env:OTOPCUA_HEIDENHAIN_X", CredentialReferenceKind.OutOfScope)]
[InlineData("file:appsettings.json", CredentialReferenceKind.OutOfScope)]
[InlineData("FILE:x", CredentialReferenceKind.OutOfScope)]
public void Parse_classifies_against_the_galaxy_scope(string? value, CredentialReferenceKind expected) =>
CredentialReferences.Parse(value, Galaxy, out _).ShouldBe(expected);
[Fact]
public void Scopes_do_not_overlap_across_drivers()
{
CredentialReferences.Parse("secret:heidenhain/x", Galaxy, out _).ShouldBe(CredentialReferenceKind.OutOfScope);
CredentialReferences.Parse("secret:galaxy/x", DriverCredentialPolicy.OpcUaClientScope, out _)
.ShouldBe(CredentialReferenceKind.OutOfScope);
CredentialReferences.Parse("env:OTOPCUA_GALAXY_X", DriverCredentialPolicy.HeidenhainScope, out _)
.ShouldBe(CredentialReferenceKind.OutOfScope);
}
[Theory]
[InlineData(CredentialLiteralPolicy.Allowed, "plain", true)]
[InlineData(CredentialLiteralPolicy.Refused, "plain", false)]
[InlineData(CredentialLiteralPolicy.Refused, "dev:x", false)]
[InlineData(CredentialLiteralPolicy.DevPrefixOnly, "dev:x", true)]
[InlineData(CredentialLiteralPolicy.DevPrefixOnly, "plain", false)]
[InlineData(CredentialLiteralPolicy.Allowed, "env:ConnectionStrings__ConfigDb", false)]
public void IsAcceptable_applies_the_literal_policy(CredentialLiteralPolicy policy, string value, bool expected) =>
CredentialReferences.IsAcceptable(value, Galaxy, policy).ShouldBe(expected);
/// The fail-closed core: an out-of-scope reference throws before either source is consulted.
[Theory]
[InlineData("env:ConnectionStrings__ConfigDb")]
[InlineData("secret:otopcua/historian/api-key")]
[InlineData("file:appsettings.json")]
public async Task Resolve_never_reads_for_an_out_of_scope_reference(string value)
{
var reads = 0;
var ex = await Should.ThrowAsync(async () => await CredentialReferences.ResolveAsync(
value, Galaxy, "F", CredentialLiteralPolicy.Allowed,
(_, _) => { reads++; return ValueTask.FromResult("LEAKED"); },
_ => { reads++; return "LEAKED"; },
CancellationToken.None));
reads.ShouldBe(0);
ex.Message.ShouldContain("'secret:galaxy/'");
ex.Message.ShouldContain("'env:OTOPCUA_GALAXY_'");
ex.Message.ShouldNotContain("LEAKED");
}
[Fact]
public async Task Resolve_reads_scoped_references_and_fails_closed_when_absent()
{
(await CredentialReferences.ResolveAsync("secret:galaxy/a", Galaxy, "F", CredentialLiteralPolicy.Refused,
(n, _) => ValueTask.FromResult(n == "galaxy/a" ? "v" : null), _ => null, CancellationToken.None))
.ShouldBe("v");
(await CredentialReferences.ResolveAsync("env:OTOPCUA_GALAXY_A", Galaxy, "F", CredentialLiteralPolicy.Refused,
null, n => n == "OTOPCUA_GALAXY_A" ? "e" : null, CancellationToken.None)).ShouldBe("e");
(await Should.ThrowAsync(async () => await CredentialReferences.ResolveAsync(
"secret:galaxy/missing", Galaxy, "F", CredentialLiteralPolicy.Refused,
(_, _) => ValueTask.FromResult(null), null, CancellationToken.None))).Message.ShouldContain("absent");
(await Should.ThrowAsync(async () => await CredentialReferences.ResolveAsync(
"env:OTOPCUA_GALAXY_UNSET", Galaxy, "F", CredentialLiteralPolicy.Refused,
null, _ => null, CancellationToken.None))).Message.ShouldContain("not set");
(await Should.ThrowAsync(async () => await CredentialReferences.ResolveAsync(
"secret:galaxy/a", Galaxy, "F", CredentialLiteralPolicy.Refused,
null, null, CancellationToken.None))).Message.ShouldContain("no secret store");
}
[Fact]
public void DescribeProblems_scans_every_depth_case_insensitively_and_never_echoes_the_value()
{
const string json = """
{
"gateway": { "endpoint": "http://evil:5001", "apiKeySecretRef": "env:ConnectionStrings__ConfigDb" },
"Devices": [ { "AlarmAck": { "FallbackPasswordSecretRef": "hunter2" } } ]
}
""";
var problems = DriverCredentialPolicy.DescribeProblems(DriverTypeNames.Galaxy, json);
problems.Count.ShouldBe(2);
problems.ShouldContain(p => p.StartsWith("gateway.apiKeySecretRef", StringComparison.Ordinal));
problems.ShouldContain(p => p.StartsWith("Devices[0].AlarmAck.FallbackPasswordSecretRef", StringComparison.Ordinal));
problems.ShouldAllBe(p => !p.Contains("hunter2") && !p.Contains("ConnectionStrings__ConfigDb"));
}
[Theory]
[InlineData(DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":"secret:galaxy/main/apikey"},"AlarmAck":{"FallbackPasswordSecretRef":"dev:x"}}""")]
[InlineData(DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":"literal-key"}}""")]
[InlineData(DriverTypeNames.OpcUaClient, """{"Password":"plain","UserCertificatePassword":"secret:opcuaclient/plant/cert"}""")]
[InlineData(DriverTypeNames.Heidenhain, """{"Access":{"InspectPassword":"env:OTOPCUA_HEIDENHAIN_INSPECT"}}""")]
[InlineData(DriverTypeNames.Modbus, """{"Password":"env:ConnectionStrings__ConfigDb"}""")]
[InlineData(DriverTypeNames.Galaxy, "")]
public void DescribeProblems_accepts_namespaced_and_unrelated_configs(string driverType, string json) =>
DriverCredentialPolicy.DescribeProblems(driverType, json).ShouldBeEmpty();
[Theory]
[InlineData(DriverTypeNames.OpcUaClient, """{"Password":"secret:galaxy/main/apikey"}""")]
[InlineData(DriverTypeNames.OpcUaClient, """{"UserCertificatePassword":"env:ServerHistorian__ApiKey"}""")]
[InlineData(DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":"file:C:\\ProgramData\\x.txt"}}""")]
[InlineData(DriverTypeNames.Galaxy, """{"Gateway":{"ApiKeySecretRef":42}}""")]
[InlineData(DriverTypeNames.Heidenhain, """{"InspectPassword":"plain"}""")]
[InlineData(DriverTypeNames.Galaxy, "{not json")]
public void DescribeProblems_refuses_out_of_namespace_configs(string driverType, string json) =>
DriverCredentialPolicy.DescribeProblems(driverType, json).ShouldNotBeEmpty();
}