Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/SubscribeCommandTests.cs
Joseph Doherty f9bc301c33 Client rename residuals: lmxopcua-cli → otopcua-cli + LmxOpcUaClient → OtOpcUaClient with migration shim. Closes task #208 (the executable-name + LocalAppData-folder slice that was called out in Client.CLI.md / Client.UI.md as a deliberately-deferred residual of the Phase 0 rename). Six source references flipped to the canonical OtOpcUaClient spelling: Program.cs CliFx executable name + description (lmxopcua-cli → otopcua-cli), DefaultApplicationConfigurationFactory.cs ApplicationName + ApplicationUri (LmxOpcUaClient + urn:localhost:LmxOpcUaClient → OtOpcUaClient + urn:localhost:OtOpcUaClient), OpcUaClientService.CreateSessionAsync session-name arg, ConnectionSettings.CertificateStorePath default, MainWindowViewModel.CertificateStorePath default, JsonSettingsService.SettingsDir. Two consuming tests (ConnectionSettingsTests + MainWindowViewModelTests) updated to assert the new canonical name. New ClientStoragePaths static helper at src/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs is the migration shim — single entry point for the PKI root + pki subpath, runs a one-shot legacy-folder probe on first resolution: if {LocalAppData}/LmxOpcUaClient/ exists + {LocalAppData}/OtOpcUaClient/ does not, Directory.Move renames it in place (atomic on NTFS within the same volume) so trusted server certs + saved connection settings persist across the rename without operator action. Idempotent per-process via a Lock-guarded _migrationChecked flag so repeated CertificateStorePath getter calls on the hot path pay no IO cost beyond the first. Fresh-install path (neither folder exists) + already-migrated path (only canonical exists) + manual-override path (both exist — developer has set up something explicit) are all no-ops that leave state alone. IOException on the Directory.Move is swallowed + logged as a false return so a concurrent peer process losing the race doesn't crash the consumer; the losing process falls back to whatever state exists. Five new ClientStoragePathsTests assert: GetRoot ends with canonical name under LocalAppData, GetPkiPath nests pki under root, CanonicalFolderName is OtOpcUaClient, LegacyFolderName is LmxOpcUaClient (the migration contract — a typo here would leak the legacy folder past the shim), repeat invocation returns false after first-touch arms the in-process guard. Doc-side residual-explanation notes in docs/Client.CLI.md + docs/Client.UI.md are dropped now that the rename is real; replaced with a short "pre-#208 dev boxes migrate automatically on first launch" note that points at ClientStoragePaths. Sample CLI invocations in Client.CLI.md updated via sed from lmxopcua-cli to otopcua-cli across every command block (14 replacements). Pre-existing staleness in SubscribeCommandTests.Execute_PrintsSubscriptionMessage surfaced during the test run — the CLI's subscribe command has long since switched to an aggregate "Subscribed to {count}/{total} nodes (interval: ...)" output format but the test still asserted the original single-node form. Updated the assertion to match current output + added a comment explaining the change; this is unrelated to the rename but was blocking a green Client.CLI.Tests run. Full solution build 0 errors; Client.Shared.Tests 136/136 + 5 new shim tests passing; Client.UI.Tests 98/98; Client.CLI.Tests 52/52 (was 51/52 before the subscribe-test fix). No Admin/Core/Server changes — this touches only the client layer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 01:50:40 -04:00

108 lines
3.5 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Client.CLI.Commands;
using ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.Fakes;
namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class SubscribeCommandTests
{
[Fact]
public async Task Execute_SubscribesWithCorrectParameters()
{
var fakeService = new FakeOpcUaClientService();
var factory = new FakeOpcUaClientServiceFactory(fakeService);
var command = new SubscribeCommand(factory)
{
Url = "opc.tcp://localhost:4840",
NodeId = "ns=2;s=TestVar",
Interval = 500
};
using var console = TestConsoleHelper.CreateConsole();
// The subscribe command waits for cancellation. We need to cancel it.
// Use the console's cancellation to trigger stop.
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
// Give it a moment to subscribe, then cancel
await Task.Delay(100);
console.RequestCancellation();
await task;
fakeService.SubscribeCalls.Count.ShouldBe(1);
fakeService.SubscribeCalls[0].IntervalMs.ShouldBe(500);
fakeService.SubscribeCalls[0].NodeId.Identifier.ShouldBe("TestVar");
}
[Fact]
public async Task Execute_UnsubscribesOnCancellation()
{
var fakeService = new FakeOpcUaClientService();
var factory = new FakeOpcUaClientServiceFactory(fakeService);
var command = new SubscribeCommand(factory)
{
Url = "opc.tcp://localhost:4840",
NodeId = "ns=2;s=TestVar"
};
using var console = TestConsoleHelper.CreateConsole();
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
await Task.Delay(100);
console.RequestCancellation();
await task;
fakeService.UnsubscribeCalls.Count.ShouldBe(1);
}
[Fact]
public async Task Execute_DisconnectsInFinally()
{
var fakeService = new FakeOpcUaClientService();
var factory = new FakeOpcUaClientServiceFactory(fakeService);
var command = new SubscribeCommand(factory)
{
Url = "opc.tcp://localhost:4840",
NodeId = "ns=2;s=TestVar"
};
using var console = TestConsoleHelper.CreateConsole();
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
await Task.Delay(100);
console.RequestCancellation();
await task;
fakeService.DisconnectCalled.ShouldBeTrue();
fakeService.DisposeCalled.ShouldBeTrue();
}
[Fact]
public async Task Execute_PrintsSubscriptionMessage()
{
var fakeService = new FakeOpcUaClientService();
var factory = new FakeOpcUaClientServiceFactory(fakeService);
var command = new SubscribeCommand(factory)
{
Url = "opc.tcp://localhost:4840",
NodeId = "ns=2;s=TestVar",
Interval = 2000
};
using var console = TestConsoleHelper.CreateConsole();
var task = Task.Run(async () => { await command.ExecuteAsync(console); });
await Task.Delay(100);
console.RequestCancellation();
await task;
var output = TestConsoleHelper.GetOutput(console);
// CLI now prints aggregate form "Subscribed to {count}/{total} nodes (interval: ...)" rather than
// the single-node form the original test asserted — the command supports multi-node now.
output.ShouldContain("Subscribed to 1/1 nodes (interval: 2000ms)");
}
}