Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/Adapters/MxGatewayDataConnectionSecretsTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

200 lines
9.8 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;
// 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;
/// <summary>Initializes the fake resolver with the value to return.</summary>
/// <param name="value">The secret value returned for every lookup, or null to simulate an unresolvable secret.</param>
public FakeSecretResolver(string? value) => _value = value;
/// <summary>Gets the names of the secrets that were requested.</summary>
public List<string> Requested { get; } = new();
/// <summary>Looks up the secret by name.</summary>
/// <param name="name">The secret name.</param>
/// <param name="ct">A token to cancel the operation.</param>
/// <returns>The secret, or null when none matches.</returns>
public Task<string?> GetAsync(SecretName name, CancellationToken ct)
{
Requested.Add(name.Value);
return Task.FromResult(_value);
}
}
/// <summary>Verifies that connecting resolves a secret reference used as the API key.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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);
}
/// <summary>Verifies that a padded or case-variant secret reference resolves to the name the scanner ships.</summary>
/// <param name="apiKey">The padded or case-variant API key reference.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
[Theory]
[InlineData("secret:mxgw-key ")]
[InlineData(" SECRET: mxgw-key")]
[InlineData("Secret:mxgw-key")]
public async Task ConnectAsync_resolves_padded_or_case_variant_ref_to_the_name_the_scanner_ships(string apiKey)
{
// The adapter and central's SecretReferenceScanner share one parser,
// so the name looked up is exactly the name that was deployed.
var shipped = Assert.Single(ZB.MOM.WW.ScadaBridge.Commons.Types.Secrets.SecretReferenceScanner.Scan([
new ZB.MOM.WW.ScadaBridge.Commons.Messages.Artifacts.DataConnectionArtifact(
"GW", "MxGateway", System.Text.Json.JsonSerializer.Serialize(new { ApiKey = apiKey }), null, 3),
])).Name;
var fake = new FakeMxGatewayClient();
var resolver = new FakeSecretResolver("resolved-plaintext-key");
var adapter = new MxGatewayDataConnection(fake, NullLogger<MxGatewayDataConnection>.Instance, resolver);
await adapter.ConnectAsync(Details(apiKey));
Assert.Equal("mxgw-key", shipped);
Assert.Equal([shipped], resolver.Requested);
Assert.Equal("resolved-plaintext-key", fake.ConnectedWith!.ApiKey);
}
/// <summary>Verifies that connecting fails closed on a secret reference with no name.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectAsync_fails_closed_on_a_secret_ref_with_no_name()
{
var fake = new FakeMxGatewayClient();
var resolver = new FakeSecretResolver("SHOULD-NOT-BE-USED");
var adapter = new MxGatewayDataConnection(fake, NullLogger<MxGatewayDataConnection>.Instance, resolver);
await Assert.ThrowsAsync<InvalidOperationException>(() => adapter.ConnectAsync(Details("secret: ")));
Assert.Null(fake.ConnectedWith);
Assert.Empty(resolver.Requested);
}
/// <summary>Verifies that a literal API key passes through unchanged and the resolver is not called.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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);
}
/// <summary>Verifies that connecting fails closed when the secret cannot be resolved.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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);
}
/// <summary>Verifies that connecting fails closed on a secret reference when no resolver is available.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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.
/// <summary>Verifies that the factory resolved from DI threads the registered secret resolver into the MxGateway adapter.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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);
}
}