using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Hosting; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests; /// /// Smoke tests for the PR 4.0 driver skeleton. The IDriver shape, factory parsing, /// and lifecycle methods land in PR 4.0; capability bodies (browse / read / write / /// subscribe / health forwarder / probe watcher) are tested in PRs 4.1–4.7 each /// against their own seam. /// public sealed class GalaxyDriverFactoryTests { private const string MinimalConfig = """ { "Gateway": { "Endpoint": "https://mxgw.test:5001", "ApiKeySecretRef": "galaxy:apiKey" }, "MxAccess": { "ClientName": "OtOpcUa-A" } } """; [Fact] public void CreateInstance_ParsesMinimalConfig_AndAppliesDefaults() { var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-instance-a", MinimalConfig); driver.DriverInstanceId.ShouldBe("galaxy-instance-a"); driver.DriverType.ShouldBe(GalaxyDriverFactoryExtensions.DriverTypeName); driver.Options.Gateway.Endpoint.ShouldBe("https://mxgw.test:5001"); driver.Options.Gateway.ApiKeySecretRef.ShouldBe("galaxy:apiKey"); driver.Options.Gateway.UseTls.ShouldBeTrue(); driver.Options.Gateway.ConnectTimeoutSeconds.ShouldBe(10); driver.Options.MxAccess.ClientName.ShouldBe("OtOpcUa-A"); driver.Options.MxAccess.PublishingIntervalMs.ShouldBe(1000); driver.Options.Repository.DiscoverPageSize.ShouldBe(5000); driver.Options.Reconnect.ReplayOnSessionLost.ShouldBeTrue(); } [Fact] public void CreateInstance_OverridesDefaults_FromFullConfig() { const string fullConfig = """ { "Gateway": { "Endpoint": "https://mxgw.prod:5001", "ApiKeySecretRef": "secret:abc", "UseTls": false, "CaCertificatePath": "C:/certs/ca.crt", "ConnectTimeoutSeconds": 5, "DefaultCallTimeoutSeconds": 3, "StreamTimeoutSeconds": 60 }, "MxAccess": { "ClientName": "OtOpcUa-Prod", "PublishingIntervalMs": 250, "WriteUserId": 17 }, "Repository": { "DiscoverPageSize": 1000, "WatchDeployEvents": false }, "Reconnect": { "InitialBackoffMs": 100, "MaxBackoffMs": 5000, "ReplayOnSessionLost": false } } """; var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-prod", fullConfig); driver.Options.Gateway.UseTls.ShouldBeFalse(); driver.Options.Gateway.CaCertificatePath.ShouldBe("C:/certs/ca.crt"); driver.Options.Gateway.ConnectTimeoutSeconds.ShouldBe(5); driver.Options.MxAccess.PublishingIntervalMs.ShouldBe(250); driver.Options.MxAccess.WriteUserId.ShouldBe(17); driver.Options.Repository.DiscoverPageSize.ShouldBe(1000); driver.Options.Repository.WatchDeployEvents.ShouldBeFalse(); driver.Options.Reconnect.InitialBackoffMs.ShouldBe(100); driver.Options.Reconnect.ReplayOnSessionLost.ShouldBeFalse(); } [Fact] public void CreateInstance_MissingEndpoint_Throws() { const string bad = """{"Gateway":{"ApiKeySecretRef":"x"},"MxAccess":{"ClientName":"y"}}"""; Should.Throw( () => GalaxyDriverFactoryExtensions.CreateInstance("g", bad)).Message.ShouldContain("Gateway.Endpoint"); } [Fact] public void CreateInstance_MissingApiKey_Throws() { const string bad = """{"Gateway":{"Endpoint":"x"},"MxAccess":{"ClientName":"y"}}"""; Should.Throw( () => GalaxyDriverFactoryExtensions.CreateInstance("g", bad)).Message.ShouldContain("ApiKeySecretRef"); } [Fact] public void CreateInstance_MissingClientName_Throws() { const string bad = """{"Gateway":{"Endpoint":"x","ApiKeySecretRef":"y"}}"""; Should.Throw( () => GalaxyDriverFactoryExtensions.CreateInstance("g", bad)).Message.ShouldContain("MxAccess.ClientName"); } [Fact] public void Register_AddsFactoryToRegistry() { var registry = new DriverFactoryRegistry(); GalaxyDriverFactoryExtensions.Register(registry); registry.RegisteredTypes.ShouldContain(GalaxyDriverFactoryExtensions.DriverTypeName); var factory = registry.TryGet(GalaxyDriverFactoryExtensions.DriverTypeName); factory.ShouldNotBeNull(); var driver = factory!.Invoke("galaxy-x", MinimalConfig); driver.ShouldNotBeNull(); driver.DriverInstanceId.ShouldBe("galaxy-x"); driver.DriverType.ShouldBe(GalaxyDriverFactoryExtensions.DriverTypeName); } [Fact] public async Task DriverLifecycle_InitializeShutdown_ToggleHealth() { var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-x", MinimalConfig); driver.GetHealth().State.ShouldBe(DriverState.Unknown); await driver.InitializeAsync(MinimalConfig, CancellationToken.None); driver.GetHealth().State.ShouldBe(DriverState.Healthy); driver.GetHealth().LastSuccessfulRead.ShouldNotBeNull(); await driver.ShutdownAsync(CancellationToken.None); driver.GetHealth().State.ShouldBe(DriverState.Unknown); driver.GetMemoryFootprint().ShouldBe(0); await driver.FlushOptionalCachesAsync(CancellationToken.None); // no-op shouldn't throw } [Fact] public async Task ReinitializeAsync_RefreshesHealth() { var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-x", MinimalConfig); await driver.InitializeAsync(MinimalConfig, CancellationToken.None); var firstStamp = driver.GetHealth().LastSuccessfulRead!.Value; // Force a measurable clock delta so the comparison is stable on fast machines. await Task.Delay(20); await driver.ReinitializeAsync(MinimalConfig, CancellationToken.None); driver.GetHealth().State.ShouldBe(DriverState.Healthy); driver.GetHealth().LastSuccessfulRead!.Value.ShouldBeGreaterThan(firstStamp); } [Fact] public void Dispose_IsIdempotent_AndShutdownAfterDisposeIsHarmless() { var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-x", MinimalConfig); driver.Dispose(); Should.NotThrow(() => driver.Dispose()); } [Fact] public async Task InitializeAfterDispose_Throws() { var driver = GalaxyDriverFactoryExtensions.CreateInstance("galaxy-x", MinimalConfig); driver.Dispose(); await Should.ThrowAsync(() => driver.InitializeAsync(MinimalConfig, CancellationToken.None)); } }