141 lines
6.2 KiB
C#
141 lines
6.2 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
|
|
using ZB.MOM.WW.Secrets.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests.Adapters;
|
|
|
|
// G-3 / Task 9: the MxGateway per-endpoint ApiKey may hold a `secret:<name>` reference
|
|
// resolved via ISecretResolver at connect time. Literal keys pass through unchanged
|
|
// (back-compat); an unresolvable / missing secret, or a secret ref with no resolver
|
|
// available, fails closed (no connection established with an empty/null key).
|
|
[Collection("DataConnectionManagerActor")]
|
|
public class MxGatewayDataConnectionSecretsTests
|
|
{
|
|
private static Dictionary<string, string> Details(string apiKey) => new()
|
|
{
|
|
["Endpoint"] = "http://gw:5000",
|
|
["ApiKey"] = apiKey,
|
|
["ClientName"] = "client-a",
|
|
["WriteUserId"] = "0",
|
|
["ReadTimeoutMs"] = "2000",
|
|
};
|
|
|
|
private sealed class FakeSecretResolver : ISecretResolver
|
|
{
|
|
private readonly string? _value;
|
|
public FakeSecretResolver(string? value) => _value = value;
|
|
public List<string> Requested { get; } = new();
|
|
|
|
public Task<string?> GetAsync(SecretName name, CancellationToken ct)
|
|
{
|
|
Requested.Add(name.Value);
|
|
return Task.FromResult(_value);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConnectAsync_resolves_secret_ref_apikey()
|
|
{
|
|
var fake = new FakeMxGatewayClient();
|
|
var resolver = new FakeSecretResolver("resolved-plaintext-key");
|
|
var adapter = new MxGatewayDataConnection(fake, NullLogger<MxGatewayDataConnection>.Instance, resolver);
|
|
|
|
await adapter.ConnectAsync(Details("secret:mxgateway/testconn/api-key"));
|
|
|
|
Assert.Equal(ConnectionHealth.Connected, adapter.Status);
|
|
Assert.NotNull(fake.ConnectedWith);
|
|
Assert.Equal("resolved-plaintext-key", fake.ConnectedWith!.ApiKey);
|
|
Assert.Contains("mxgateway/testconn/api-key", resolver.Requested);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConnectAsync_passes_literal_apikey_unchanged_and_does_not_call_resolver()
|
|
{
|
|
var fake = new FakeMxGatewayClient();
|
|
var resolver = new FakeSecretResolver("SHOULD-NOT-BE-USED");
|
|
var adapter = new MxGatewayDataConnection(fake, NullLogger<MxGatewayDataConnection>.Instance, resolver);
|
|
|
|
await adapter.ConnectAsync(Details("literal-api-key"));
|
|
|
|
Assert.Equal("literal-api-key", fake.ConnectedWith!.ApiKey);
|
|
Assert.Empty(resolver.Requested);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConnectAsync_fails_closed_when_secret_unresolvable()
|
|
{
|
|
var fake = new FakeMxGatewayClient();
|
|
var resolver = new FakeSecretResolver(null); // secret not found / tombstoned
|
|
var adapter = new MxGatewayDataConnection(fake, NullLogger<MxGatewayDataConnection>.Instance, resolver);
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
|
() => adapter.ConnectAsync(Details("secret:mxgateway/testconn/missing")));
|
|
|
|
// Fail-closed: the underlying client was never connected and status is not Connected.
|
|
Assert.Null(fake.ConnectedWith);
|
|
Assert.NotEqual(ConnectionHealth.Connected, adapter.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConnectAsync_fails_closed_on_secret_ref_with_no_resolver()
|
|
{
|
|
var fake = new FakeMxGatewayClient();
|
|
var adapter = new MxGatewayDataConnection(fake, NullLogger<MxGatewayDataConnection>.Instance, secretResolver: null);
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
|
() => adapter.ConnectAsync(Details("secret:mxgateway/testconn/api-key")));
|
|
|
|
Assert.Null(fake.ConnectedWith);
|
|
Assert.NotEqual(ConnectionHealth.Connected, adapter.Status);
|
|
}
|
|
|
|
// DI-threading seam (code-review follow-up): the four tests above construct the adapter
|
|
// directly with an explicit resolver, so they don't prove the factory→adapter DI wiring
|
|
// G-3 actually relies on. This test resolves IDataConnectionFactory from a real container
|
|
// (AddDataConnectionLayer + a registered fake ISecretResolver, no AddZbSecrets), creates a
|
|
// "MxGateway" adapter through it, and drives a secret:-ref ConnectAsync. The real client
|
|
// then fails to reach a non-existent gateway (expected, ignored) — but resolution happens
|
|
// BEFORE any connect attempt, so the fake resolver being invoked proves the greediest
|
|
// 3-arg DataConnectionFactory ctor picked up the DI-registered resolver and threaded it in.
|
|
// Had it been null, ConnectAsync would have thrown the "no ISecretResolver" fail-closed
|
|
// path instead and the resolver would never have been called.
|
|
[Fact]
|
|
public async Task Factory_from_DI_threads_registered_ISecretResolver_into_MxGateway_adapter()
|
|
{
|
|
var fake = new FakeSecretResolver("resolved-from-di");
|
|
var services = new ServiceCollection();
|
|
services.AddLogging();
|
|
services.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build());
|
|
services.AddDataConnectionLayer();
|
|
services.AddSingleton<ISecretResolver>(fake);
|
|
using var provider = services.BuildServiceProvider();
|
|
|
|
var factory = provider.GetRequiredService<IDataConnectionFactory>();
|
|
|
|
// Point at a fast-refusing loopback port so the real client's connect fails quickly.
|
|
var details = Details("secret:mxgateway/testconn/api-key");
|
|
details["Endpoint"] = "http://127.0.0.1:1";
|
|
|
|
await using var adapter = factory.Create("MxGateway", details);
|
|
Assert.IsType<MxGatewayDataConnection>(adapter);
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
try
|
|
{
|
|
await adapter.ConnectAsync(details, cts.Token);
|
|
}
|
|
catch
|
|
{
|
|
// The real RealMxGatewayClient cannot reach 127.0.0.1:1 — irrelevant to this test.
|
|
}
|
|
|
|
// Resolution runs before the connect attempt: the resolver being invoked proves the
|
|
// factory threaded the DI-registered ISecretResolver into the adapter.
|
|
Assert.Contains("mxgateway/testconn/api-key", fake.Requested);
|
|
}
|
|
}
|