Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/GalaxyDriverFactoryTests.cs
Joseph Doherty 8fe7c8bea6 refactor(driver-galaxy): switch to sibling-repo MxGateway client + drop vendored libs
The sibling mxaccessgw repo (clients/dotnet/) restored a proper client
library + contracts under the new ZB.MOM.WW.MxGateway namespace, so the
binary-vendoring stopgap from PR Driver.Galaxy-016 can unwind via plan #1
of libs/README.md.

- csproj: replace <Reference HintPath="libs\MxGateway.*.dll"> with a
  ProjectReference into ..\..\..\..\mxaccessgw\clients\dotnet  ZB.MOM.WW.MxGateway.Client\. The five backfill PackageReference shims
  (Google.Protobuf, Grpc.Core.Api, Grpc.Net.Client, Polly.Core,
  Microsoft.Extensions.Logging.Abstractions) are now transitive again.
- Source: 'using MxGateway.X' -> 'using ZB.MOM.WW.MxGateway.X' across
  19 driver files + 14 test files. No fully-qualified MxGateway.* usages
  in code, so no behavioural changes — purely a using-prefix flip.
- libs/: deleted MxGateway.Client.dll, MxGateway.Contracts.dll, README.md
  (orphan after the unwind).

Verified: dotnet build clean (Release), all 245 Driver.Galaxy unit tests
pass, OtOpcUa service running with the new client DLL loaded
(opc.tcp://localhost:4840/OtOpcUa, no FileNotFound/TypeLoad/
MissingMethod in startup logs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 14:55:15 -04:00

255 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests;
/// <summary>
/// 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.14.7 each
/// against their own seam.
/// </summary>
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<InvalidOperationException>(
() => 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<InvalidOperationException>(
() => GalaxyDriverFactoryExtensions.CreateInstance("g", bad)).Message.ShouldContain("ApiKeySecretRef");
}
[Fact]
public void CreateInstance_MissingClientName_Throws()
{
const string bad = """{"Gateway":{"Endpoint":"x","ApiKeySecretRef":"y"}}""";
Should.Throw<InvalidOperationException>(
() => 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()
{
// Inject a no-op subscriber seam so InitializeAsync skips BuildProductionRuntimeAsync
// (no gw connect attempt). The factory tests only care about the lifecycle health
// toggle; real-runtime wire-up is exercised in PR 4.W's BuildProductionRuntime tests.
using var driver = new GalaxyDriver(
"galaxy-x", BuildOptions(), hierarchySource: null, dataReader: null,
dataWriter: null, subscriber: new NoopSubscriber());
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_WhenConfigIsEquivalent()
{
// Driver.Galaxy-013: ReinitializeAsync now compares the incoming JSON to the
// live options. An equivalent config is accepted and refreshes health; a
// non-equivalent reapply throws NotSupportedException (covered in
// GalaxyDriverInfrastructureTests.ReinitializeAsync_RejectsNonEquivalentConfigChange).
// Build a config JSON whose parsed shape equals BuildOptions() so the
// equivalence check passes.
const string equivalentConfig = """
{
"Gateway": { "Endpoint": "https://mxgw.test:5001", "ApiKeySecretRef": "key" },
"MxAccess": { "ClientName": "OtOpcUa-A" }
}
""";
using var driver = new GalaxyDriver(
"galaxy-x", BuildOptions(), hierarchySource: null, dataReader: null,
dataWriter: null, subscriber: new NoopSubscriber());
await driver.InitializeAsync(equivalentConfig, 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(equivalentConfig, CancellationToken.None);
driver.GetHealth().State.ShouldBe(DriverState.Healthy);
driver.GetHealth().LastSuccessfulRead!.Value.ShouldBeGreaterThan(firstStamp);
}
private static GalaxyDriverOptions BuildOptions() => new(
new GalaxyGatewayOptions("https://mxgw.test:5001", "key"),
new GalaxyMxAccessOptions("OtOpcUa-A"),
new GalaxyRepositoryOptions(),
new GalaxyReconnectOptions());
/// <summary>
/// Minimum-surface <see cref="IGalaxySubscriber"/> seam — enough to satisfy
/// <see cref="GalaxyDriver.InitializeAsync"/>'s "skip production-runtime build" branch
/// without driving any actual subscribe/event-pump traffic.
/// </summary>
private sealed class NoopSubscriber : IGalaxySubscriber
{
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
IReadOnlyList<string> fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken)
=> Task.FromResult<IReadOnlyList<SubscribeResult>>([]);
public Task UnsubscribeBulkAsync(IReadOnlyList<int> itemHandles, CancellationToken cancellationToken)
=> Task.CompletedTask;
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.CompletedTask;
yield break;
}
}
[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<ObjectDisposedException>(() =>
driver.InitializeAsync(MinimalConfig, CancellationToken.None));
}
[Fact]
public void DriverImplementsAllPhase4Capabilities()
{
// PR 4.W contract pin — every capability the in-process Galaxy driver must
// expose for parity with GalaxyProxyDriver. If any of these regress, the
// Galaxy:Backend flag flip in PR 7.1 will silently lose surface.
var driver = GalaxyDriverFactoryExtensions.CreateInstance("g", MinimalConfig);
driver.ShouldBeAssignableTo<ITagDiscovery>();
driver.ShouldBeAssignableTo<IReadable>();
driver.ShouldBeAssignableTo<IWritable>();
driver.ShouldBeAssignableTo<ISubscribable>();
driver.ShouldBeAssignableTo<IRediscoverable>();
driver.ShouldBeAssignableTo<IHostConnectivityProbe>();
}
[Fact]
public async Task GetHostStatuses_AfterInitWithSeam_ReturnsEmptySnapshot()
{
// PR 4.W wire-up assertion: when InitializeAsync skips the production-runtime
// build (seam injected), no transport-forwarder or probe-watcher pushes a
// status, so the aggregator snapshot is empty. The forwarder + watcher have
// their own unit tests in PR 4.7.
using var driver = new GalaxyDriver(
"g", BuildOptions(), hierarchySource: null, dataReader: null,
dataWriter: null, subscriber: new NoopSubscriber());
await driver.InitializeAsync(MinimalConfig, CancellationToken.None);
driver.GetHostStatuses().ShouldBeEmpty();
}
[Fact]
public void DriverType_IsGalaxyMxGateway_NotLegacyGalaxy()
{
// Must match GalaxyProxyDriver's DriverType ("Galaxy") side-by-side without
// collision so DriverInstanceBootstrapper can resolve both factories.
var driver = GalaxyDriverFactoryExtensions.CreateInstance("g", MinimalConfig);
driver.DriverType.ShouldBe("GalaxyMxGateway");
}
}