Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/GalaxyDriverApiKeyResolverTests.cs
Joseph Doherty 42f41fbe50 v2-mxgw follow-ups: production reads, secret resolution, perf knobs
Lands the five concrete code-level follow-ups identified after Phase 7.1:

#1 GalaxyDriver.ReadAsync now works in production. Previously threw
   NotSupportedException when no test reader was injected. New path
   subscribes through the existing SubscriptionRegistry + EventPump,
   waits for the first OnDataChange per item handle (gw pushes the
   initial value after SubscribeBulk), then unsubscribes. Tags the gw
   rejects up front, or that don't publish before the caller's CT
   fires, return Bad-status snapshots in input order so callers still
   get one snapshot per requested reference.

#2 ResolveApiKey() routes Gateway.ApiKeySecretRef through three forms:
   env:NAME, file:PATH, or literal-string fallback. A future DPAPI arm
   slots in here without touching the call site.

#3 GatewayGalaxySubscriber actually honors bufferedUpdateIntervalMs now
   (was being silently dropped). Calls SetBufferedUpdateInterval via
   the gw's MxCommandKind.SetBufferedUpdateInterval before SubscribeBulk
   when the requested interval differs from the cached last-applied
   value. Soft-fails on a non-Ok protocol status (the SubscribeBulk
   still succeeds at gw cadence).

#4 GalaxyMxAccessOptions.EventPumpChannelCapacity surfaces the bounded-
   channel size through DriverConfig JSON, defaulting to 50_000.

#5 Stale doc-comments in HostStatusAggregator and GatewayGalaxySubscriber
   describing follow-ups that already shipped.

Tests: +6 (read subscribe-once happy path + rejected-tag fallback;
five resolver scenarios). Total Galaxy driver tests now 180/180 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 17:27:24 -04:00

89 lines
2.7 KiB
C#

using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests;
/// <summary>
/// Follow-up #2 — pins the three resolution forms supported by
/// <see cref="GalaxyDriver.ResolveApiKey"/>: <c>env:NAME</c>, <c>file:PATH</c>,
/// and the literal-string fallback. A future DPAPI arm slots in here without
/// touching the call site.
/// </summary>
public sealed class GalaxyDriverApiKeyResolverTests
{
[Fact]
public void Literal_string_is_returned_unchanged()
{
GalaxyDriver.ResolveApiKey("plain-text-key").ShouldBe("plain-text-key");
}
[Fact]
public void Env_prefix_resolves_to_environment_variable()
{
const string name = "OTOPCUA_TEST_GALAXY_API_KEY";
Environment.SetEnvironmentVariable(name, "key-from-env");
try
{
GalaxyDriver.ResolveApiKey($"env:{name}").ShouldBe("key-from-env");
}
finally
{
Environment.SetEnvironmentVariable(name, null);
}
}
[Fact]
public void Env_prefix_unset_variable_throws_with_descriptive_message()
{
const string name = "OTOPCUA_TEST_GALAXY_API_KEY_UNSET";
Environment.SetEnvironmentVariable(name, null);
var ex = Should.Throw<InvalidOperationException>(() =>
GalaxyDriver.ResolveApiKey($"env:{name}"));
ex.Message.ShouldContain(name);
ex.Message.ShouldContain("unset");
}
[Fact]
public void File_prefix_resolves_to_trimmed_file_contents()
{
var path = Path.Combine(Path.GetTempPath(), $"galaxy-key-{Guid.NewGuid():N}.txt");
File.WriteAllText(path, " key-from-file \n");
try
{
GalaxyDriver.ResolveApiKey($"file:{path}").ShouldBe("key-from-file");
}
finally
{
File.Delete(path);
}
}
[Fact]
public void File_prefix_missing_path_throws()
{
var path = Path.Combine(Path.GetTempPath(), $"does-not-exist-{Guid.NewGuid():N}.txt");
var ex = Should.Throw<InvalidOperationException>(() =>
GalaxyDriver.ResolveApiKey($"file:{path}"));
ex.Message.ShouldContain(path);
ex.Message.ShouldContain("doesn't exist");
}
[Fact]
public void File_prefix_empty_file_throws()
{
var path = Path.Combine(Path.GetTempPath(), $"galaxy-key-empty-{Guid.NewGuid():N}.txt");
File.WriteAllText(path, " \n ");
try
{
var ex = Should.Throw<InvalidOperationException>(() =>
GalaxyDriver.ResolveApiKey($"file:{path}"));
ex.Message.ShouldContain("empty");
}
finally
{
File.Delete(path);
}
}
}