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; /// /// Guards the Test Connect wiring contract: every driver type editable in the AdminUI must have /// a registered , resolvable from the same DI container that hosts the /// admin-operations cluster singleton. The singleton is role-pinned to admin, 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". /// /// /// ⚠️ The expected set is derived from , not hand-written. /// It used to be a literal array kept "in sync" by comment, and it drifted the moment `Sql` /// (9b5a9687) and `Mqtt` (f79d13e2) shipped: the count assertion below went red /// reporting 12 where 10 expected — 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 no 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. /// 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().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() .Select(p => p.DriverType) .Where(t => !DriverTypeNames.All.Contains(t, StringComparer.OrdinalIgnoreCase)) .ToList(); orphans.ShouldBeEmpty($"Probes for unknown driver types: {string.Join(", ", orphans)}"); } /// /// Two driver types are spelled differently by the constant and by the AdminUI's own key /// (TwinCAT/TwinCat, FOCAS/Focas), so the lookup must fold case. The /// derived tests above cannot cover this — they compare the constants against themselves. /// [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().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().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); } }