5bbe1e3c7a
v2-ci / unit-tests (push) Successful in 1h9m7s
The Test-Connect probe set is resolved inside the AdminOperations Akka starter; resolving ISecretResolver there walks AddZbSecrets -> IAuditWriter -> ActorAuditWriter, whose ctor needs AuditWriterActorKey (registered later in the same builder). Found by the docker-dev live gate. Regression test: resolving the probe set never resolves ISecretResolver (fails before the fix). Also: TagRowProblem doc comment re-attached; Heidenhain credential help text names the secret:heidenhain/ and env:OTOPCUA_HEIDENHAIN_ namespaces.
129 lines
6.2 KiB
C#
129 lines
6.2 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Host.Drivers;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// Guards the Test Connect wiring contract: every driver type editable in the AdminUI must have
|
|
/// a registered <see cref="IDriverProbe"/>, resolvable from the same DI container that hosts the
|
|
/// <c>admin-operations</c> cluster singleton. The singleton is role-pinned to <c>admin</c>, so on
|
|
/// a split-role deployment (the MAIN cluster's admin-only nodes) the probes must be wired by the
|
|
/// admin path — not only the driver path — or every Test Connect button returns
|
|
/// "No probe registered for driver type X".
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// ⚠️ <b>The expected set is derived from <see cref="DriverTypeNames.All"/>, not hand-written.</b>
|
|
/// It used to be a literal array kept "in sync" by comment, and it drifted the moment `Sql`
|
|
/// (<c>9b5a9687</c>) and `Mqtt` (<c>f79d13e2</c>) shipped: the count assertion below went red
|
|
/// reporting <i>12 where 10 expected</i> — a true statement that named neither driver and read like
|
|
/// the probes were wrong rather than the list. Worse is the direction it could NOT catch: a new
|
|
/// driver type registered with <b>no</b> probe passed silently, because a hand-written list grows
|
|
/// only when someone remembers to grow it, and this test exists precisely for the case where nobody
|
|
/// did. Deriving from the constants makes the guard bidirectional and self-maintaining.
|
|
/// </remarks>
|
|
public sealed class DriverProbeRegistrationTests
|
|
{
|
|
[Fact]
|
|
public void AddOtOpcUaDriverProbes_registers_a_probe_for_every_driver_type()
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddOtOpcUaDriverProbes();
|
|
|
|
using var sp = services.BuildServiceProvider();
|
|
var probes = sp.GetServices<IDriverProbe>().ToList();
|
|
|
|
// No duplicate DriverType — AdminOperationsActor builds a dictionary keyed by DriverType
|
|
// (case-insensitive) and would throw on a duplicate key, crashing the singleton.
|
|
var byType = probes.ToDictionary(p => p.DriverType, StringComparer.OrdinalIgnoreCase);
|
|
|
|
var missing = DriverTypeNames.All.Where(t => !byType.ContainsKey(t)).ToList();
|
|
missing.ShouldBeEmpty(
|
|
"Driver types with no IDriverProbe — Test Connect returns 'No probe registered for driver "
|
|
+ $"type X' for each: {string.Join(", ", missing)}");
|
|
}
|
|
|
|
[Fact]
|
|
public void No_probe_is_registered_for_a_driver_type_that_does_not_exist()
|
|
{
|
|
// The other direction. A probe whose DriverType matches no DriverTypeNames constant can never
|
|
// be reached — AdminOperationsActor looks probes up by the authored DriverType — so it is a
|
|
// typo or a leftover from a renamed driver, and it reads as coverage while providing none.
|
|
var services = new ServiceCollection();
|
|
services.AddOtOpcUaDriverProbes();
|
|
|
|
using var sp = services.BuildServiceProvider();
|
|
|
|
var orphans = sp.GetServices<IDriverProbe>()
|
|
.Select(p => p.DriverType)
|
|
.Where(t => !DriverTypeNames.All.Contains(t, StringComparer.OrdinalIgnoreCase))
|
|
.ToList();
|
|
|
|
orphans.ShouldBeEmpty($"Probes for unknown driver types: {string.Join(", ", orphans)}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Two driver types are spelled differently by the constant and by the AdminUI's own key
|
|
/// (<c>TwinCAT</c>/<c>TwinCat</c>, <c>FOCAS</c>/<c>Focas</c>), so the lookup must fold case. The
|
|
/// derived tests above cannot cover this — they compare the constants against themselves.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("TwinCat", DriverTypeNames.TwinCAT)]
|
|
[InlineData("Focas", DriverTypeNames.FOCAS)]
|
|
[InlineData("modbus", DriverTypeNames.Modbus)]
|
|
public void A_probe_resolves_by_a_case_drifted_driver_type_key(string authoredKey, string canonical)
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddOtOpcUaDriverProbes();
|
|
|
|
using var sp = services.BuildServiceProvider();
|
|
var byType = sp.GetServices<IDriverProbe>().ToDictionary(p => p.DriverType, StringComparer.OrdinalIgnoreCase);
|
|
|
|
byType.ShouldContainKey(authoredKey);
|
|
byType[authoredKey].DriverType.ShouldBe(canonical);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddOtOpcUaDriverProbes_is_idempotent()
|
|
{
|
|
// A fused admin,driver node calls the registration from both the driver-factory path and the
|
|
// admin path. TryAddEnumerable must de-dup so the probe set stays unique (else the actor's
|
|
// ToDictionary throws).
|
|
var services = new ServiceCollection();
|
|
services.AddOtOpcUaDriverProbes();
|
|
services.AddOtOpcUaDriverProbes();
|
|
|
|
using var sp = services.BuildServiceProvider();
|
|
var probes = sp.GetServices<IDriverProbe>().ToList();
|
|
|
|
var distinctTypes = probes.Select(p => p.DriverType).Distinct(StringComparer.OrdinalIgnoreCase).Count();
|
|
probes.Count.ShouldBe(distinctTypes, "Duplicate IDriverProbe registrations — TryAddEnumerable should de-dup.");
|
|
distinctTypes.ShouldBe(DriverTypeNames.All.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Resolving_the_probe_set_never_resolves_the_secret_resolver()
|
|
{
|
|
// #803 live gate: the probe set is resolved inside the AdminOperations Akka starter, where
|
|
// ISecretResolver -> AddZbSecrets -> IAuditWriter -> ActorAuditWriter throws (its actor key is
|
|
// registered later in the same builder). A probe that resolved the secret resolver eagerly crash-looped
|
|
// every central node at boot. Model that starter-time graph with a resolver factory that throws.
|
|
var resolved = 0;
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton<ZB.MOM.WW.Secrets.Abstractions.ISecretResolver>(_ =>
|
|
{
|
|
resolved++;
|
|
throw new InvalidOperationException("ISecretResolver must not be resolved while building the probe set");
|
|
});
|
|
services.AddOtOpcUaDriverProbes();
|
|
|
|
using var sp = services.BuildServiceProvider();
|
|
var probes = sp.GetServices<IDriverProbe>().ToList();
|
|
|
|
probes.ShouldNotBeEmpty();
|
|
resolved.ShouldBe(0);
|
|
}
|
|
}
|