Compare commits
3 Commits
docs-refre
...
rename-cli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9bc301c33 | ||
|
|
12d748c4f3 | ||
| e9b1d107ab |
@@ -87,13 +87,14 @@ The server supports non-transparent warm/hot redundancy via the `Redundancy` sec
|
||||
|
||||
## LDAP Authentication
|
||||
|
||||
The server uses LDAP-based user authentication via the `Authentication.Ldap` section in `appsettings.json`. When enabled, credentials are validated by LDAP bind against a GLAuth server (installed at `C:\publish\glauth\`), and LDAP group membership maps to OPC UA permissions: `ReadOnly` (browse/read), `WriteOperate` (write FreeAccess/Operate attributes), `WriteTune` (write Tune attributes), `WriteConfigure` (write Configure attributes), `AlarmAck` (alarm acknowledgment). `LdapAuthenticationProvider` implements both `IUserAuthenticationProvider` and `IRoleProvider`. See `docs/Security.md` for the full guide and `C:\publish\glauth\auth.md` for LDAP user/group reference.
|
||||
The server uses LDAP-based user authentication via the `Authentication.Ldap` section in `appsettings.json`. When enabled, credentials are validated by LDAP bind against a GLAuth server (installed at `C:\publish\glauth\`), and LDAP group membership maps to OPC UA permissions: `ReadOnly` (browse/read), `WriteOperate` (write FreeAccess/Operate attributes), `WriteTune` (write Tune attributes), `WriteConfigure` (write Configure attributes), `AlarmAck` (alarm acknowledgment). `LdapUserAuthenticator` (`src/ZB.MOM.WW.OtOpcUa.Server/Security/LdapUserAuthenticator.cs`) implements `IUserAuthenticator`. See `docs/Security.md` for the full guide and `C:\publish\glauth\auth.md` for LDAP user/group reference.
|
||||
|
||||
## Library Preferences
|
||||
|
||||
- **Logging**: Serilog with rolling daily file sink
|
||||
- **Unit tests**: xUnit + Shouldly for assertions
|
||||
- **Service hosting**: TopShelf (Windows service install/uninstall/run as console)
|
||||
- **Service hosting (Server, Admin)**: .NET generic host with `AddWindowsService` (decision #30 — replaced TopShelf in v2; see `src/ZB.MOM.WW.OtOpcUa.Server/OpcUaServerService.cs`)
|
||||
- **Service hosting (Galaxy.Host)**: plain console app wrapped by NSSM (`.NET Framework 4.8 x86` — required by MXAccess COM bitness)
|
||||
- **OPC UA**: OPC Foundation UA .NET Standard stack (https://github.com/opcfoundation/ua-.netstandard) — NuGet: `OPCFoundation.NetStandard.Opc.Ua.Server`
|
||||
|
||||
## OPC UA .NET Standard Documentation
|
||||
|
||||
@@ -14,7 +14,7 @@ dotnet build
|
||||
dotnet run -- <command> [options]
|
||||
```
|
||||
|
||||
The executable name is still `lmxopcua-cli` — a residual from the pre-v2 rename (`Program.cs:SetExecutableName`). Scripts + operator muscle memory depend on the name; flipping it to `otopcua-cli` is a follow-up that also needs to move the client-side PKI store folder (<code>{LocalAppData}/LmxOpcUaClient/pki/</code> — used by the shared client for its application certificate) so trust relationships survive the rename.
|
||||
The executable name is `otopcua-cli`. Dev boxes carrying a pre-task-#208 install may still have the legacy `{LocalAppData}/LmxOpcUaClient/` folder on disk; on first launch of any post-#208 CLI or UI build, `ClientStoragePaths` (`src/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs`) migrates it to `{LocalAppData}/OtOpcUaClient/` automatically so trusted certificates + saved settings survive the rename.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -46,7 +46,7 @@ All commands accept these options:
|
||||
When `-U` and `-P` are provided, the shared service passes a `UserIdentity(username, password)` to the OPC UA session. Without credentials, anonymous identity is used.
|
||||
|
||||
```bash
|
||||
lmxopcua-cli write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42 -U operator -P op123
|
||||
otopcua-cli write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42 -U operator -P op123
|
||||
```
|
||||
|
||||
### Failover
|
||||
@@ -54,20 +54,20 @@ lmxopcua-cli write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42 -U opera
|
||||
When `-F` is provided, the shared service tries the primary URL first, then each failover URL in order. For long-running commands (`subscribe`, `alarms`), the service monitors the session via keep-alive and automatically reconnects to the next available server on failure.
|
||||
|
||||
```bash
|
||||
lmxopcua-cli connect -u opc.tcp://localhost:4840/OtOpcUa -F opc.tcp://localhost:4841/OtOpcUa
|
||||
otopcua-cli connect -u opc.tcp://localhost:4840/OtOpcUa -F opc.tcp://localhost:4841/OtOpcUa
|
||||
```
|
||||
|
||||
### Transport Security
|
||||
|
||||
When `sign` or `encrypt` is specified, the shared service:
|
||||
|
||||
1. Ensures a client application certificate exists under `{LocalAppData}/LmxOpcUaClient/pki/` (auto-created if missing)
|
||||
1. Ensures a client application certificate exists under `{LocalAppData}/OtOpcUaClient/pki/` (auto-created if missing; pre-rename `LmxOpcUaClient/` is migrated in place on first launch)
|
||||
2. Discovers server endpoints and selects one matching the requested security mode
|
||||
3. Prefers `Basic256Sha256` when multiple matching endpoints exist
|
||||
4. Fails with a clear error if no matching endpoint is found
|
||||
|
||||
```bash
|
||||
lmxopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -S encrypt -U admin -P secret -r -d 2
|
||||
otopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -S encrypt -U admin -P secret -r -d 2
|
||||
```
|
||||
|
||||
### Verbose Logging
|
||||
@@ -81,7 +81,7 @@ The `--verbose` flag switches Serilog output from `Warning` to `Debug` level, sh
|
||||
Tests connectivity to an OPC UA server. Creates a session, prints connection metadata, and disconnects.
|
||||
|
||||
```bash
|
||||
lmxopcua-cli connect -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123
|
||||
otopcua-cli connect -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123
|
||||
```
|
||||
|
||||
Output:
|
||||
@@ -99,7 +99,7 @@ Connection successful.
|
||||
Reads the current value of a single node and prints the value, status code, and timestamps.
|
||||
|
||||
```bash
|
||||
lmxopcua-cli read -u opc.tcp://localhost:4840/OtOpcUa -n "ns=3;s=DEV.ScanState" -U admin -P admin123
|
||||
otopcua-cli read -u opc.tcp://localhost:4840/OtOpcUa -n "ns=3;s=DEV.ScanState" -U admin -P admin123
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
@@ -121,7 +121,7 @@ Server Time: 2026-03-30T19:58:38.0971257Z
|
||||
Writes a value to a node. The shared service reads the current value first to determine the target data type, then converts the supplied string value using `ValueConverter.ConvertValue()`.
|
||||
|
||||
```bash
|
||||
lmxopcua-cli write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42
|
||||
otopcua-cli write -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -v 42
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
@@ -135,10 +135,10 @@ Browses the OPC UA address space starting from the Objects folder or a specified
|
||||
|
||||
```bash
|
||||
# Browse top-level Objects folder
|
||||
lmxopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123
|
||||
otopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123
|
||||
|
||||
# Browse a specific node recursively to depth 3
|
||||
lmxopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123 -r -d 3 -n "ns=3;s=ZB"
|
||||
otopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123 -r -d 3 -n "ns=3;s=ZB"
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
@@ -152,7 +152,7 @@ lmxopcua-cli browse -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123 -r
|
||||
Monitors a node for value changes using OPC UA subscriptions. Prints each data change notification with timestamp, value, and status code. Runs until Ctrl+C, then unsubscribes and disconnects cleanly.
|
||||
|
||||
```bash
|
||||
lmxopcua-cli subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -i 500
|
||||
otopcua-cli subscribe -u opc.tcp://localhost:4840 -n "ns=2;s=MyNode" -i 500
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
@@ -166,12 +166,12 @@ Reads historical data from a node. Supports raw history reads and aggregate (pro
|
||||
|
||||
```bash
|
||||
# Raw history
|
||||
lmxopcua-cli historyread -u opc.tcp://localhost:4840/OtOpcUa \
|
||||
otopcua-cli historyread -u opc.tcp://localhost:4840/OtOpcUa \
|
||||
-n "ns=1;s=TestMachine_001.TestHistoryValue" \
|
||||
--start "2026-03-25" --end "2026-03-30"
|
||||
|
||||
# Aggregate: 1-hour average
|
||||
lmxopcua-cli historyread -u opc.tcp://localhost:4840/OtOpcUa \
|
||||
otopcua-cli historyread -u opc.tcp://localhost:4840/OtOpcUa \
|
||||
-n "ns=1;s=TestMachine_001.TestHistoryValue" \
|
||||
--start "2026-03-25" --end "2026-03-30" \
|
||||
--aggregate Average --interval 3600000
|
||||
@@ -203,10 +203,10 @@ Subscribes to alarm events on a node. Prints structured alarm output including s
|
||||
|
||||
```bash
|
||||
# Subscribe to alarm events on the Server node
|
||||
lmxopcua-cli alarms -u opc.tcp://localhost:4840/OtOpcUa
|
||||
otopcua-cli alarms -u opc.tcp://localhost:4840/OtOpcUa
|
||||
|
||||
# Subscribe to a specific source node with condition refresh
|
||||
lmxopcua-cli alarms -u opc.tcp://localhost:4840/OtOpcUa \
|
||||
otopcua-cli alarms -u opc.tcp://localhost:4840/OtOpcUa \
|
||||
-n "ns=1;s=TestMachine_001" --refresh
|
||||
```
|
||||
|
||||
@@ -221,7 +221,7 @@ lmxopcua-cli alarms -u opc.tcp://localhost:4840/OtOpcUa \
|
||||
Reads the OPC UA redundancy state from a server: redundancy mode, service level, server URIs, and application URI.
|
||||
|
||||
```bash
|
||||
lmxopcua-cli redundancy -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123
|
||||
otopcua-cli redundancy -u opc.tcp://localhost:4840/OtOpcUa -U admin -P admin123
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
@@ -65,7 +65,7 @@ The top bar provides the endpoint URL, Connect, and Disconnect buttons. The **Co
|
||||
|
||||
### Settings Persistence
|
||||
|
||||
Connection settings are saved to `{LocalAppData}/LmxOpcUaClient/settings.json` after each successful connection and on window close. The folder name is a residual from the pre-v2 rename (the `Client.Shared` session factory still calls itself `LmxOpcUaClient` at `OpcUaClientService.cs:428`); renaming to `OtOpcUaClient` is a follow-up that needs a migration shim so existing users don't lose their settings on upgrade. The settings are reloaded on next launch, including:
|
||||
Connection settings are saved to `{LocalAppData}/OtOpcUaClient/settings.json` after each successful connection and on window close. Dev boxes upgrading from a pre-task-#208 build still have the legacy `LmxOpcUaClient/` folder on disk; `ClientStoragePaths` in `Client.Shared` moves it to the canonical path on first launch so existing trusted certs + saved settings persist without operator action. The settings are reloaded on next launch, including:
|
||||
|
||||
- All connection parameters
|
||||
- Active subscription node IDs (restored after reconnection)
|
||||
|
||||
@@ -9,7 +9,7 @@ return await new CliApplicationBuilder()
|
||||
if (type.IsSubclassOf(typeof(CommandBase))) return Activator.CreateInstance(type, CommandBase.DefaultFactory)!;
|
||||
return Activator.CreateInstance(type)!;
|
||||
})
|
||||
.SetExecutableName("lmxopcua-cli")
|
||||
.SetDescription("LmxOpcUa CLI - command-line client for the LmxOpcUa OPC UA server")
|
||||
.SetExecutableName("otopcua-cli")
|
||||
.SetDescription("OtOpcUa CLI - command-line client for the OtOpcUa OPC UA server")
|
||||
.Build()
|
||||
.RunAsync(args);
|
||||
@@ -18,8 +18,8 @@ internal sealed class DefaultApplicationConfigurationFactory : IApplicationConfi
|
||||
|
||||
var config = new ApplicationConfiguration
|
||||
{
|
||||
ApplicationName = "LmxOpcUaClient",
|
||||
ApplicationUri = "urn:localhost:LmxOpcUaClient",
|
||||
ApplicationName = "OtOpcUaClient",
|
||||
ApplicationUri = "urn:localhost:OtOpcUaClient",
|
||||
ApplicationType = ApplicationType.Client,
|
||||
SecurityConfiguration = new SecurityConfiguration
|
||||
{
|
||||
@@ -60,7 +60,7 @@ internal sealed class DefaultApplicationConfigurationFactory : IApplicationConfi
|
||||
{
|
||||
var app = new ApplicationInstance
|
||||
{
|
||||
ApplicationName = "LmxOpcUaClient",
|
||||
ApplicationName = "OtOpcUaClient",
|
||||
ApplicationType = ApplicationType.Client,
|
||||
ApplicationConfiguration = config
|
||||
};
|
||||
|
||||
90
src/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs
Normal file
90
src/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Client.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the canonical under-LocalAppData folder for the shared OPC UA client's PKI
|
||||
/// store + persisted settings. Renamed from <c>LmxOpcUaClient</c> to <c>OtOpcUaClient</c>
|
||||
/// in task #208; a one-shot migration shim moves a pre-rename folder in place on first
|
||||
/// resolution so existing developer boxes keep their trusted server certs + saved
|
||||
/// connection settings on upgrade.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Thread-safe: the rename uses <see cref="Directory.Move"/> which is atomic on NTFS
|
||||
/// within the same volume. The lock guarantees the migration runs at most once per
|
||||
/// process even under concurrent first-touch from CLI + UI.
|
||||
/// </remarks>
|
||||
public static class ClientStoragePaths
|
||||
{
|
||||
/// <summary>Canonical client folder name. Post-#208.</summary>
|
||||
public const string CanonicalFolderName = "OtOpcUaClient";
|
||||
|
||||
/// <summary>Pre-#208 folder name. Used only by the migration shim.</summary>
|
||||
public const string LegacyFolderName = "LmxOpcUaClient";
|
||||
|
||||
private static readonly Lock _migrationLock = new();
|
||||
private static bool _migrationChecked;
|
||||
|
||||
/// <summary>
|
||||
/// Absolute path to the client's top-level folder under LocalApplicationData. Runs the
|
||||
/// one-shot legacy-folder migration before returning so callers that depend on this
|
||||
/// path (PKI store, settings file) find their existing state at the canonical name.
|
||||
/// </summary>
|
||||
public static string GetRoot()
|
||||
{
|
||||
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
var canonical = Path.Combine(localAppData, CanonicalFolderName);
|
||||
MigrateLegacyFolderIfNeeded(localAppData, canonical);
|
||||
return canonical;
|
||||
}
|
||||
|
||||
/// <summary>Subfolder for the application's PKI store — used by both CLI + UI.</summary>
|
||||
public static string GetPkiPath() => Path.Combine(GetRoot(), "pki");
|
||||
|
||||
/// <summary>
|
||||
/// Expose the migration probe for tests + for callers that want to check whether the
|
||||
/// legacy folder still exists without forcing the rename. Returns true when a legacy
|
||||
/// folder existed + was moved to canonical, false when no migration was needed or
|
||||
/// canonical was already present.
|
||||
/// </summary>
|
||||
public static bool TryRunLegacyMigration()
|
||||
{
|
||||
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
var canonical = Path.Combine(localAppData, CanonicalFolderName);
|
||||
return MigrateLegacyFolderIfNeeded(localAppData, canonical);
|
||||
}
|
||||
|
||||
private static bool MigrateLegacyFolderIfNeeded(string localAppData, string canonical)
|
||||
{
|
||||
// Fast-path out of the lock when the migration has already been attempted this process
|
||||
// — saves the IO on every subsequent call, + the migration is idempotent within the
|
||||
// same process anyway.
|
||||
if (_migrationChecked) return false;
|
||||
|
||||
lock (_migrationLock)
|
||||
{
|
||||
if (_migrationChecked) return false;
|
||||
_migrationChecked = true;
|
||||
|
||||
var legacy = Path.Combine(localAppData, LegacyFolderName);
|
||||
|
||||
// Only migrate when the legacy folder is present + canonical isn't. Either of the
|
||||
// other three combinations (neither / only-canonical / both) means migration
|
||||
// should NOT run: no-op fresh install, already-migrated, or manual state the
|
||||
// developer has set up — don't clobber.
|
||||
if (!Directory.Exists(legacy)) return false;
|
||||
if (Directory.Exists(canonical)) return false;
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Move(legacy, canonical);
|
||||
return true;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Concurrent another-process-moved-it or volume-boundary or permissions — leave
|
||||
// the legacy folder alone; callers that need it can either re-run migration
|
||||
// manually or point CertificateStorePath explicitly.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,11 +41,11 @@ public sealed class ConnectionSettings
|
||||
public bool AutoAcceptCertificates { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Path to the certificate store. Defaults to a subdirectory under LocalApplicationData.
|
||||
/// Path to the certificate store. Defaults to a subdirectory under LocalApplicationData
|
||||
/// resolved via <see cref="ClientStoragePaths"/> so the one-shot legacy-folder migration
|
||||
/// runs before the path is returned.
|
||||
/// </summary>
|
||||
public string CertificateStorePath { get; set; } = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"LmxOpcUaClient", "pki");
|
||||
public string CertificateStorePath { get; set; } = ClientStoragePaths.GetPkiPath();
|
||||
|
||||
/// <summary>
|
||||
/// Validates the settings and throws if any required values are missing or invalid.
|
||||
|
||||
@@ -425,7 +425,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
: new UserIdentity();
|
||||
|
||||
var sessionTimeoutMs = (uint)(settings.SessionTimeoutSeconds * 1000);
|
||||
return await _sessionFactory.CreateSessionAsync(config, endpoint, "LmxOpcUaClient", sessionTimeoutMs, identity,
|
||||
return await _sessionFactory.CreateSessionAsync(config, endpoint, "OtOpcUaClient", sessionTimeoutMs, identity,
|
||||
ct);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.OtOpcUa.Client.Shared;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services;
|
||||
|
||||
@@ -7,9 +8,9 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services;
|
||||
/// </summary>
|
||||
public sealed class JsonSettingsService : ISettingsService
|
||||
{
|
||||
private static readonly string SettingsDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"LmxOpcUaClient");
|
||||
// ClientStoragePaths.GetRoot runs the one-shot legacy-folder migration so pre-#208
|
||||
// developer boxes pick up their existing settings.json on first launch post-rename.
|
||||
private static readonly string SettingsDir = ClientStoragePaths.GetRoot();
|
||||
|
||||
private static readonly string SettingsPath = Path.Combine(SettingsDir, "settings.json");
|
||||
|
||||
|
||||
@@ -21,9 +21,7 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
|
||||
[ObservableProperty] private bool _autoAcceptCertificates = true;
|
||||
|
||||
[ObservableProperty] private string _certificateStorePath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"LmxOpcUaClient", "pki");
|
||||
[ObservableProperty] private string _certificateStorePath = ClientStoragePaths.GetPkiPath();
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(ConnectCommand))]
|
||||
|
||||
@@ -101,7 +101,8 @@ public class SubscribeCommandTests
|
||||
await task;
|
||||
|
||||
var output = TestConsoleHelper.GetOutput(console);
|
||||
output.ShouldContain("Subscribed to ns=2;s=TestVar (interval: 2000ms)");
|
||||
output.ShouldContain("Unsubscribed.");
|
||||
// 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)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Client.Shared;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ClientStoragePathsTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetRoot_ReturnsCanonicalFolderName_UnderLocalAppData()
|
||||
{
|
||||
var root = ClientStoragePaths.GetRoot();
|
||||
root.ShouldEndWith(ClientStoragePaths.CanonicalFolderName);
|
||||
root.ShouldContain(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetPkiPath_NestsPkiUnderRoot()
|
||||
{
|
||||
var pki = ClientStoragePaths.GetPkiPath();
|
||||
pki.ShouldEndWith(Path.Combine(ClientStoragePaths.CanonicalFolderName, "pki"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanonicalFolderName_IsOtOpcUaClient()
|
||||
{
|
||||
ClientStoragePaths.CanonicalFolderName.ShouldBe("OtOpcUaClient");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LegacyFolderName_IsLmxOpcUaClient()
|
||||
{
|
||||
// The shim depends on this specific spelling — a typo here would leak the legacy
|
||||
// folder past the migration + break every dev-box upgrade.
|
||||
ClientStoragePaths.LegacyFolderName.ShouldBe("LmxOpcUaClient");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryRunLegacyMigration_Returns_False_On_Repeat_Invocation()
|
||||
{
|
||||
// Once the guard in-process has fired, subsequent calls short-circuit to false
|
||||
// regardless of filesystem state. This is the behaviour that keeps the migration
|
||||
// cheap on hot paths (CertificateStorePath property getter is called frequently).
|
||||
_ = ClientStoragePaths.GetRoot(); // arms the guard
|
||||
ClientStoragePaths.TryRunLegacyMigration().ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ public class ConnectionSettingsTests
|
||||
settings.SecurityMode.ShouldBe(SecurityMode.None);
|
||||
settings.SessionTimeoutSeconds.ShouldBe(60);
|
||||
settings.AutoAcceptCertificates.ShouldBeTrue();
|
||||
settings.CertificateStorePath.ShouldContain("LmxOpcUaClient");
|
||||
settings.CertificateStorePath.ShouldContain("OtOpcUaClient");
|
||||
settings.CertificateStorePath.ShouldContain("pki");
|
||||
}
|
||||
|
||||
|
||||
@@ -252,7 +252,7 @@ public class MainWindowViewModelTests
|
||||
_vm.FailoverUrls.ShouldBeNull();
|
||||
_vm.SessionTimeoutSeconds.ShouldBe(60);
|
||||
_vm.AutoAcceptCertificates.ShouldBeTrue();
|
||||
_vm.CertificateStorePath.ShouldContain("LmxOpcUaClient");
|
||||
_vm.CertificateStorePath.ShouldContain("OtOpcUaClient");
|
||||
_vm.CertificateStorePath.ShouldContain("pki");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user