docs: backfill XML documentation across 756 files
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
This commit is contained in:
Joseph Doherty
2026-05-28 08:10:17 -04:00
parent f9fc7dd2e1
commit 64e3fbe035
756 changed files with 9876 additions and 96 deletions
@@ -7,6 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class AlarmsCommandTests
{
/// <summary>Verifies that Execute subscribes to alarms.</summary>
[Fact]
public async Task Execute_SubscribesToAlarms()
{
@@ -31,6 +32,7 @@ public class AlarmsCommandTests
fakeService.SubscribeAlarmsCalls[0].SourceNodeId.ShouldBeNull();
}
/// <summary>Verifies that Execute with node passes source node ID.</summary>
[Fact]
public async Task Execute_WithNode_PassesSourceNodeId()
{
@@ -55,6 +57,7 @@ public class AlarmsCommandTests
fakeService.SubscribeAlarmsCalls[0].SourceNodeId!.Identifier.ShouldBe("AlarmSource");
}
/// <summary>Verifies that Execute with refresh requests condition refresh.</summary>
[Fact]
public async Task Execute_WithRefresh_RequestsConditionRefresh()
{
@@ -79,6 +82,7 @@ public class AlarmsCommandTests
output.ShouldContain("Condition refresh requested.");
}
/// <summary>Verifies that refresh failure prints error.</summary>
[Fact]
public async Task Execute_RefreshFailure_PrintsError()
{
@@ -105,6 +109,7 @@ public class AlarmsCommandTests
output.ShouldContain("Condition refresh not supported:");
}
/// <summary>Verifies that Execute unsubscribes on cancellation.</summary>
[Fact]
public async Task Execute_UnsubscribesOnCancellation()
{
@@ -126,6 +131,7 @@ public class AlarmsCommandTests
fakeService.UnsubscribeAlarmsCalled.ShouldBeTrue();
}
/// <summary>Verifies that Execute disconnects in finally block.</summary>
[Fact]
public async Task Execute_DisconnectsInFinally()
{
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class BrowseCommandTests
{
/// <summary>Verifies that Execute prints browse results correctly.</summary>
[Fact]
public async Task Execute_PrintsBrowseResults()
{
@@ -35,6 +36,7 @@ public class BrowseCommandTests
output.ShouldContain("[Method] Method1 (NodeId: ns=2;s=Meth1)");
}
/// <summary>Verifies that Execute browses from the specified node ID.</summary>
[Fact]
public async Task Execute_BrowsesFromSpecifiedNode()
{
@@ -57,6 +59,7 @@ public class BrowseCommandTests
fakeService.BrowseNodeIds[0]!.Identifier.ShouldBe("StartNode");
}
/// <summary>Verifies that Execute browses from null node when not specified.</summary>
[Fact]
public async Task Execute_DefaultBrowsesFromNull()
{
@@ -77,6 +80,7 @@ public class BrowseCommandTests
fakeService.BrowseNodeIds[0].ShouldBeNull();
}
/// <summary>Verifies that Execute browses only a single level when not recursive.</summary>
[Fact]
public async Task Execute_NonRecursive_BrowsesSingleLevel()
{
@@ -101,6 +105,7 @@ public class BrowseCommandTests
fakeService.BrowseNodeIds.Count.ShouldBe(1);
}
/// <summary>Verifies that Execute browses child nodes when recursive flag is set.</summary>
[Fact]
public async Task Execute_Recursive_BrowsesChildren()
{
@@ -123,6 +128,7 @@ public class BrowseCommandTests
fakeService.BrowseNodeIds.Count.ShouldBeGreaterThan(1);
}
/// <summary>Verifies that Execute disconnects and disposes in the finally block.</summary>
[Fact]
public async Task Execute_DisconnectsInFinally()
{
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class CommandBaseTests
{
/// <summary>Verifies that common options map to connection settings correctly.</summary>
[Fact]
public async Task CommonOptions_MapToConnectionSettings_Correctly()
{
@@ -37,6 +38,7 @@ public class CommandBaseTests
settings.AutoAcceptCertificates.ShouldBeTrue();
}
/// <summary>Verifies that encrypt option maps to SignAndEncrypt.</summary>
[Fact]
public async Task SecurityOption_Encrypt_MapsToSignAndEncrypt()
{
@@ -54,6 +56,7 @@ public class CommandBaseTests
fakeService.LastConnectionSettings!.SecurityMode.ShouldBe(SecurityMode.SignAndEncrypt);
}
/// <summary>Verifies that none option maps to None.</summary>
[Fact]
public async Task SecurityOption_None_MapsToNone()
{
@@ -71,6 +74,7 @@ public class CommandBaseTests
fakeService.LastConnectionSettings!.SecurityMode.ShouldBe(SecurityMode.None);
}
/// <summary>Verifies that no failover URLs results in null FailoverUrls.</summary>
[Fact]
public async Task NoFailoverUrls_FailoverUrlsIsNull()
{
@@ -12,6 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
/// </summary>
public class CommandRangeValidationTests
{
/// <summary>Verifies that BrowseCommand rejects negative depth values with a command exception.</summary>
[Fact]
public async Task BrowseCommand_NegativeDepth_ThrowsCommandException()
{
@@ -28,6 +29,7 @@ public class CommandRangeValidationTests
ex.Message.ShouldContain("--depth");
}
/// <summary>Verifies that BrowseCommand rejects zero depth values with a command exception.</summary>
[Fact]
public async Task BrowseCommand_ZeroDepth_ThrowsCommandException()
{
@@ -44,6 +46,7 @@ public class CommandRangeValidationTests
ex.Message.ShouldContain("--depth");
}
/// <summary>Verifies that SubscribeCommand rejects zero interval values with a command exception.</summary>
[Fact]
public async Task SubscribeCommand_ZeroInterval_ThrowsCommandException()
{
@@ -61,6 +64,7 @@ public class CommandRangeValidationTests
ex.Message.ShouldContain("--interval");
}
/// <summary>Verifies that SubscribeCommand rejects negative interval values with a command exception.</summary>
[Fact]
public async Task SubscribeCommand_NegativeInterval_ThrowsCommandException()
{
@@ -77,6 +81,7 @@ public class CommandRangeValidationTests
await Should.ThrowAsync<CommandException>(async () => await command.ExecuteAsync(console));
}
/// <summary>Verifies that SubscribeCommand in recursive mode rejects zero max depth with a command exception.</summary>
[Fact]
public async Task SubscribeCommand_RecursiveZeroMaxDepth_ThrowsCommandException()
{
@@ -96,6 +101,7 @@ public class CommandRangeValidationTests
ex.Message.ShouldContain("--max-depth");
}
/// <summary>Verifies that SubscribeCommand rejects negative duration values with a command exception.</summary>
[Fact]
public async Task SubscribeCommand_NegativeDuration_ThrowsCommandException()
{
@@ -112,6 +118,7 @@ public class CommandRangeValidationTests
await Should.ThrowAsync<CommandException>(async () => await command.ExecuteAsync(console));
}
/// <summary>Verifies that AlarmsCommand rejects zero interval values with a command exception.</summary>
[Fact]
public async Task AlarmsCommand_ZeroInterval_ThrowsCommandException()
{
@@ -128,6 +135,7 @@ public class CommandRangeValidationTests
ex.Message.ShouldContain("--interval");
}
/// <summary>Verifies that HistoryReadCommand rejects negative max values with a command exception.</summary>
[Fact]
public async Task HistoryReadCommand_NegativeMax_ThrowsCommandException()
{
@@ -145,6 +153,7 @@ public class CommandRangeValidationTests
ex.Message.ShouldContain("--max");
}
/// <summary>Verifies that HistoryReadCommand rejects zero interval values with a command exception.</summary>
[Fact]
public async Task HistoryReadCommand_ZeroInterval_ThrowsCommandException()
{
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class ConnectCommandTests
{
/// <summary>Verifies that execute prints connection info.</summary>
[Fact]
public async Task Execute_PrintsConnectionInfo()
{
@@ -38,6 +39,7 @@ public class ConnectCommandTests
output.ShouldContain("Connection successful.");
}
/// <summary>Verifies that execute calls connect and disconnect.</summary>
[Fact]
public async Task Execute_CallsConnectAndDisconnect()
{
@@ -56,6 +58,7 @@ public class ConnectCommandTests
fakeService.DisposeCalled.ShouldBeTrue();
}
/// <summary>Verifies that execute disconnects on error.</summary>
[Fact]
public async Task Execute_DisconnectsOnError()
{
@@ -14,6 +14,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
/// </summary>
public class EventHandlerLifecycleTests
{
/// <summary>Verifies that SubscribeCommand detaches the DataChanged event handler after exit.</summary>
[Fact]
public async Task SubscribeCommand_AfterExit_DataChangedEventHasNoSubscribers()
{
@@ -35,6 +36,7 @@ public class EventHandlerLifecycleTests
"SubscribeCommand must detach its DataChanged handler before returning.");
}
/// <summary>Verifies that AlarmsCommand detaches the AlarmEvent handler after exit.</summary>
[Fact]
public async Task AlarmsCommand_AfterExit_AlarmEventHasNoSubscribers()
{
@@ -12,27 +12,55 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.Fakes;
public sealed class FakeOpcUaClientService : IOpcUaClientService
{
// Track calls
/// <summary>Gets a value indicating whether ConnectAsync was called.</summary>
public bool ConnectCalled { get; private set; }
/// <summary>Gets the connection settings from the most recent ConnectAsync call.</summary>
public ConnectionSettings? LastConnectionSettings { get; private set; }
/// <summary>Gets a value indicating whether DisconnectAsync was called.</summary>
public bool DisconnectCalled { get; private set; }
/// <summary>Gets a value indicating whether Dispose was called.</summary>
public bool DisposeCalled { get; private set; }
/// <summary>Gets the list of node IDs passed to ReadValueAsync calls.</summary>
public List<NodeId> ReadNodeIds { get; } = [];
/// <summary>Gets the list of (NodeId, value) pairs passed to WriteValueAsync calls.</summary>
public List<(NodeId NodeId, object Value)> WriteValues { get; } = [];
/// <summary>Gets the list of parent node IDs passed to BrowseAsync calls.</summary>
public List<NodeId?> BrowseNodeIds { get; } = [];
/// <summary>Gets the list of (NodeId, intervalMs) pairs from SubscribeAsync calls.</summary>
public List<(NodeId NodeId, int IntervalMs)> SubscribeCalls { get; } = [];
/// <summary>Gets the list of node IDs passed to UnsubscribeAsync calls.</summary>
public List<NodeId> UnsubscribeCalls { get; } = [];
/// <summary>Gets the list of (SourceNodeId, intervalMs) pairs from SubscribeAlarmsAsync calls.</summary>
public List<(NodeId? SourceNodeId, int IntervalMs)> SubscribeAlarmsCalls { get; } = [];
/// <summary>Gets a value indicating whether UnsubscribeAlarmsAsync was called.</summary>
public bool UnsubscribeAlarmsCalled { get; private set; }
/// <summary>Gets a value indicating whether RequestConditionRefreshAsync was called.</summary>
public bool RequestConditionRefreshCalled { get; private set; }
/// <summary>Gets the list of (NodeId, start, end, maxValues) tuples from HistoryReadRawAsync calls.</summary>
public List<(NodeId NodeId, DateTime Start, DateTime End, int MaxValues)> HistoryReadRawCalls { get; } = [];
/// <summary>Gets the list of history read aggregate call parameters.</summary>
public List<(NodeId NodeId, DateTime Start, DateTime End, AggregateType Aggregate, double IntervalMs)>
HistoryReadAggregateCalls { get; } =
[];
/// <summary>Gets a value indicating whether GetRedundancyInfoAsync was called.</summary>
public bool GetRedundancyInfoCalled { get; private set; }
// Configurable results
/// <summary>Gets or sets the connection info returned by ConnectAsync.</summary>
public ConnectionInfo ConnectionInfoResult { get; set; } = new(
"opc.tcp://localhost:4840",
"TestServer",
@@ -41,14 +69,17 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
"session-1",
"TestSession");
/// <summary>Gets or sets the data value returned by ReadValueAsync.</summary>
public DataValue ReadValueResult { get; set; } = new(
new Variant(42),
StatusCodes.Good,
DateTime.UtcNow,
DateTime.UtcNow);
/// <summary>Gets or sets the status code returned by WriteValueAsync.</summary>
public StatusCode WriteStatusCodeResult { get; set; } = StatusCodes.Good;
/// <summary>Gets or sets the browse results returned by BrowseAsync.</summary>
public IReadOnlyList<BrowseResult> BrowseResults { get; set; } = new List<BrowseResult>
{
new("ns=2;s=Node1", "Node1", "Object", true),
@@ -63,19 +94,30 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
/// </summary>
public Dictionary<string, IReadOnlyList<BrowseResult>> BrowseResultsByParent { get; } = new();
/// <summary>Gets or sets the history read result returned by HistoryReadRawAsync and HistoryReadAggregateAsync.</summary>
public IReadOnlyList<DataValue> HistoryReadResult { get; set; } = new List<DataValue>
{
new(new Variant(10.0), StatusCodes.Good, DateTime.UtcNow.AddHours(-1), DateTime.UtcNow),
new(new Variant(20.0), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow)
};
/// <summary>Gets or sets the redundancy info returned by GetRedundancyInfoAsync.</summary>
public RedundancyInfo RedundancyInfoResult { get; set; } = new(
"Warm", 200, ["urn:server1", "urn:server2"], "urn:app:test");
/// <summary>Gets or sets the exception thrown by ConnectAsync.</summary>
public Exception? ConnectException { get; set; }
/// <summary>Gets or sets the exception thrown by ReadValueAsync.</summary>
public Exception? ReadException { get; set; }
/// <summary>Gets or sets the exception thrown by WriteValueAsync.</summary>
public Exception? WriteException { get; set; }
/// <summary>Gets or sets the exception thrown by RequestConditionRefreshAsync.</summary>
public Exception? ConditionRefreshException { get; set; }
/// <summary>Gets or sets the exception thrown by SubscribeAsync.</summary>
public Exception? SubscribeException { get; set; }
/// <inheritdoc />
@@ -218,18 +260,24 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
}
/// <summary>Raises the DataChanged event for testing subscribe commands.</summary>
/// <param name="nodeId">The node ID string that changed.</param>
/// <param name="value">The new data value.</param>
public void RaiseDataChanged(string nodeId, DataValue value)
{
DataChanged?.Invoke(this, new DataChangedEventArgs(nodeId, value));
}
/// <summary>Raises the AlarmEvent for testing alarm commands.</summary>
/// <param name="args">The alarm event arguments.</param>
public void RaiseAlarmEvent(AlarmEventArgs args)
{
AlarmEvent?.Invoke(this, args);
}
/// <summary>Raises the ConnectionStateChanged event for testing.</summary>
/// <param name="oldState">The previous connection state.</param>
/// <param name="newState">The new connection state.</param>
/// <param name="endpointUrl">The endpoint URL.</param>
public void RaiseConnectionStateChanged(ConnectionState oldState, ConnectionState newState, string endpointUrl)
{
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(oldState, newState, endpointUrl));
@@ -9,11 +9,14 @@ public sealed class FakeOpcUaClientServiceFactory : IOpcUaClientServiceFactory
{
private readonly FakeOpcUaClientService _service;
/// <summary>Initializes a new instance of the <see cref="FakeOpcUaClientServiceFactory"/> class.</summary>
/// <param name="service">The fake OPC UA client service to return.</param>
public FakeOpcUaClientServiceFactory(FakeOpcUaClientService service)
{
_service = service;
}
/// <summary>Creates and returns the fake OPC UA client service.</summary>
public IOpcUaClientService Create()
{
return _service;
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class HistoryReadCommandTests
{
/// <summary>Verifies RawRead execution prints values.</summary>
[Fact]
public async Task Execute_RawRead_PrintsValues()
{
@@ -42,6 +43,7 @@ public class HistoryReadCommandTests
output.ShouldContain("2 values returned.");
}
/// <summary>Verifies RawRead execution calls HistoryReadRaw.</summary>
[Fact]
public async Task Execute_RawRead_CallsHistoryReadRaw()
{
@@ -62,6 +64,7 @@ public class HistoryReadCommandTests
fakeService.HistoryReadRawCalls[0].NodeId.Identifier.ShouldBe("HistNode");
}
/// <summary>Verifies AggregateRead execution calls HistoryReadAggregate.</summary>
[Fact]
public async Task Execute_AggregateRead_CallsHistoryReadAggregate()
{
@@ -83,6 +86,7 @@ public class HistoryReadCommandTests
fakeService.HistoryReadAggregateCalls[0].IntervalMs.ShouldBe(60000);
}
/// <summary>Verifies AggregateRead execution prints aggregate info.</summary>
[Fact]
public async Task Execute_AggregateRead_PrintsAggregateInfo()
{
@@ -104,6 +108,7 @@ public class HistoryReadCommandTests
output.ShouldContain("7200000");
}
/// <summary>Verifies invalid aggregate throws CommandException.</summary>
[Fact]
public async Task Execute_InvalidAggregate_ThrowsCommandException()
{
@@ -123,6 +128,7 @@ public class HistoryReadCommandTests
async () => await command.ExecuteAsync(console));
}
/// <summary>Verifies disconnect is called in finally block.</summary>
[Fact]
public async Task Execute_DisconnectsInFinally()
{
@@ -13,6 +13,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
/// </summary>
public class InputValidationErrorsTests
{
/// <summary>Verifies that HistoryReadCommand with invalid start time throws CommandException.</summary>
[Fact]
public async Task HistoryReadCommand_InvalidStartTime_ThrowsCommandException()
{
@@ -30,6 +31,7 @@ public class InputValidationErrorsTests
ex.Message.ShouldContain("--start");
}
/// <summary>Verifies that HistoryReadCommand with invalid end time throws CommandException.</summary>
[Fact]
public async Task HistoryReadCommand_InvalidEndTime_ThrowsCommandException()
{
@@ -47,6 +49,7 @@ public class InputValidationErrorsTests
ex.Message.ShouldContain("--end");
}
/// <summary>Verifies that HistoryReadCommand with invalid aggregate throws CommandException.</summary>
[Fact]
public async Task HistoryReadCommand_InvalidAggregate_ThrowsCommandException()
{
@@ -64,6 +67,7 @@ public class InputValidationErrorsTests
ex.Message.ShouldContain("aggregate", Case.Insensitive);
}
/// <summary>Verifies that ReadCommand with invalid node ID throws CommandException.</summary>
[Fact]
public async Task ReadCommand_InvalidNodeId_ThrowsCommandException()
{
@@ -80,6 +84,7 @@ public class InputValidationErrorsTests
ex.Message.ShouldContain("node", Case.Insensitive);
}
/// <summary>Verifies that SubscribeCommand with invalid node ID throws CommandException.</summary>
[Fact]
public async Task SubscribeCommand_InvalidNodeId_ThrowsCommandException()
{
@@ -14,6 +14,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
/// </summary>
public class LoggerLifecycleTests
{
/// <summary>Verifies that ConfigureLogging disposes previous logger before reassigning.</summary>
[Fact]
public async Task ConfigureLogging_DisposesPreviousLogger_BeforeReassigning()
{
@@ -48,8 +49,12 @@ public class LoggerLifecycleTests
private sealed class DisposeTrackingSink : ILogEventSink, IDisposable
{
/// <summary>Gets a value indicating whether the sink has been disposed.</summary>
public bool Disposed { get; private set; }
/// <summary>Emits a log event.</summary>
/// <param name="logEvent">The log event to emit.</param>
public void Emit(LogEvent logEvent) { }
/// <summary>Disposes the sink and marks it as disposed.</summary>
public void Dispose() => Disposed = true;
}
}
@@ -7,24 +7,28 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class NodeIdParserTests
{
/// <summary>Verifies that Parse returns null for null input.</summary>
[Fact]
public void Parse_NullInput_ReturnsNull()
{
NodeIdParser.Parse(null).ShouldBeNull();
}
/// <summary>Verifies that Parse returns null for empty string input.</summary>
[Fact]
public void Parse_EmptyString_ReturnsNull()
{
NodeIdParser.Parse("").ShouldBeNull();
}
/// <summary>Verifies that Parse returns null for whitespace-only input.</summary>
[Fact]
public void Parse_WhitespaceOnly_ReturnsNull()
{
NodeIdParser.Parse(" ").ShouldBeNull();
}
/// <summary>Verifies that Parse correctly parses standard NodeId format (ns=2;s=MyNode).</summary>
[Fact]
public void Parse_StandardStringFormat_ReturnsNodeId()
{
@@ -34,6 +38,7 @@ public class NodeIdParserTests
result.Identifier.ShouldBe("MyNode");
}
/// <summary>Verifies that Parse correctly parses numeric NodeId format (i=85).</summary>
[Fact]
public void Parse_NumericFormat_ReturnsNodeId()
{
@@ -42,6 +47,7 @@ public class NodeIdParserTests
result.IdType.ShouldBe(IdType.Numeric);
}
/// <summary>Verifies that Parse treats bare numeric input as namespace 0 numeric NodeId.</summary>
[Fact]
public void Parse_BareNumeric_ReturnsNamespace0NumericNodeId()
{
@@ -51,6 +57,7 @@ public class NodeIdParserTests
result.Identifier.ShouldBe((uint)85);
}
/// <summary>Verifies that Parse trims whitespace padding from input.</summary>
[Fact]
public void Parse_WithWhitespacePadding_Trims()
{
@@ -59,24 +66,28 @@ public class NodeIdParserTests
result.Identifier.ShouldBe("MyNode");
}
/// <summary>Verifies that Parse throws FormatException for invalid input.</summary>
[Fact]
public void Parse_InvalidFormat_ThrowsFormatException()
{
Should.Throw<FormatException>(() => NodeIdParser.Parse("not-a-node-id"));
}
/// <summary>Verifies that ParseRequired throws ArgumentException for null input.</summary>
[Fact]
public void ParseRequired_NullInput_ThrowsArgumentException()
{
Should.Throw<ArgumentException>(() => NodeIdParser.ParseRequired(null));
}
/// <summary>Verifies that ParseRequired throws ArgumentException for empty input.</summary>
[Fact]
public void ParseRequired_EmptyInput_ThrowsArgumentException()
{
Should.Throw<ArgumentException>(() => NodeIdParser.ParseRequired(""));
}
/// <summary>Verifies that ParseRequired returns a NodeId for valid input.</summary>
[Fact]
public void ParseRequired_ValidInput_ReturnsNodeId()
{
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class ReadCommandTests
{
/// <summary>Verifies that execute prints the read value.</summary>
[Fact]
public async Task Execute_PrintsReadValue()
{
@@ -39,6 +40,7 @@ public class ReadCommandTests
output.ShouldContain("Server Time:");
}
/// <summary>Verifies that execute calls read value with correct node ID.</summary>
[Fact]
public async Task Execute_CallsReadValueWithCorrectNodeId()
{
@@ -57,6 +59,7 @@ public class ReadCommandTests
fakeService.ReadNodeIds[0].Identifier.ShouldBe("MyVariable");
}
/// <summary>Verifies that execute disconnects in finally.</summary>
[Fact]
public async Task Execute_DisconnectsInFinally()
{
@@ -75,6 +78,7 @@ public class ReadCommandTests
fakeService.DisposeCalled.ShouldBeTrue();
}
/// <summary>Verifies that execute disconnects even on read error.</summary>
[Fact]
public async Task Execute_DisconnectsEvenOnReadError()
{
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class RedundancyCommandTests
{
/// <summary>Verifies that Execute prints redundancy information correctly.</summary>
[Fact]
public async Task Execute_PrintsRedundancyInfo()
{
@@ -34,6 +35,7 @@ public class RedundancyCommandTests
output.ShouldContain("Application URI: urn:app:myserver");
}
/// <summary>Verifies that Execute omits the Server URIs section when none are present.</summary>
[Fact]
public async Task Execute_NoServerUris_OmitsUriSection()
{
@@ -58,6 +60,7 @@ public class RedundancyCommandTests
output.ShouldContain("Application URI: urn:app:standalone");
}
/// <summary>Verifies that Execute calls GetRedundancyInfo on the service.</summary>
[Fact]
public async Task Execute_CallsGetRedundancyInfo()
{
@@ -74,6 +77,7 @@ public class RedundancyCommandTests
fakeService.GetRedundancyInfoCalled.ShouldBeTrue();
}
/// <summary>Verifies that Execute disconnects and disposes in the finally block.</summary>
[Fact]
public async Task Execute_DisconnectsInFinally()
{
@@ -13,6 +13,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
/// </summary>
public class SubscribeCommandSummaryTests
{
/// <summary>Verifies that nodes with no updates are counted separately from suspects.</summary>
[Fact]
public async Task Summary_NodeWithNoUpdate_IsCountedAsNeverNotAsNeverWentBad()
{
@@ -39,6 +40,7 @@ public class SubscribeCommandSummaryTests
output.ShouldContain("--- Nodes that never received an update at all ---");
}
/// <summary>Verifies that nodes with only good values are counted as never went bad.</summary>
[Fact]
public async Task Summary_NodeReceivedOnlyGoodValues_IsCountedAsNeverWentBad()
{
@@ -70,6 +72,7 @@ public class SubscribeCommandSummaryTests
output.ShouldContain("No update received at all: 0");
}
/// <summary>Verifies that nodes with bad values are counted as ever went bad.</summary>
[Fact]
public async Task Summary_NodeReceivedBadValue_IsCountedAsEverWentBad()
{
@@ -98,6 +101,7 @@ public class SubscribeCommandSummaryTests
output.ShouldContain("NEVER went bad (suspect): 0");
}
/// <summary>Verifies that subscription auto-exits when duration expires.</summary>
[Fact]
public async Task Duration_ZeroOrPositive_AutoExits()
{
@@ -124,6 +128,7 @@ public class SubscribeCommandSummaryTests
output.ShouldContain("==================== SUMMARY ====================");
}
/// <summary>Verifies that --quiet suppresses updates but prints summary.</summary>
[Fact]
public async Task Quiet_SuppressesPerUpdateOutputButPrintsSummary()
{
@@ -154,6 +159,7 @@ public class SubscribeCommandSummaryTests
output.ShouldContain("==================== SUMMARY ====================");
}
/// <summary>Verifies that summary is written to disk when summary file is specified.</summary>
[Fact]
public async Task SummaryFile_WritesSummaryToDisk()
{
@@ -184,6 +190,7 @@ public class SubscribeCommandSummaryTests
}
}
/// <summary>Verifies that recursive flag browses subtree and subscribes every variable.</summary>
[Fact]
public async Task Recursive_BrowsesSubtreeAndSubscribesEveryVariable()
{
@@ -222,6 +229,7 @@ public class SubscribeCommandSummaryTests
output.ShouldContain("Browsing subtree of ns=2;s=Root");
}
/// <summary>Verifies that subscription failures are handled gracefully.</summary>
[Fact]
public async Task SubscribeFailure_PrintsFailedMessage_DoesNotCrash()
{
@@ -7,6 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class SubscribeCommandTests
{
/// <summary>Verifies that ExecuteAsync subscribes with the correct parameters.</summary>
[Fact]
public async Task Execute_SubscribesWithCorrectParameters()
{
@@ -35,6 +36,7 @@ public class SubscribeCommandTests
fakeService.SubscribeCalls[0].NodeId.Identifier.ShouldBe("TestVar");
}
/// <summary>Verifies that ExecuteAsync unsubscribes when cancellation is requested.</summary>
[Fact]
public async Task Execute_UnsubscribesOnCancellation()
{
@@ -57,6 +59,7 @@ public class SubscribeCommandTests
fakeService.UnsubscribeCalls.Count.ShouldBe(1);
}
/// <summary>Verifies that ExecuteAsync disconnects and disposes in a finally block.</summary>
[Fact]
public async Task Execute_DisconnectsInFinally()
{
@@ -80,6 +83,7 @@ public class SubscribeCommandTests
fakeService.DisposeCalled.ShouldBeTrue();
}
/// <summary>Verifies that ExecuteAsync prints the correct subscription message.</summary>
[Fact]
public async Task Execute_PrintsSubscriptionMessage()
{
@@ -18,6 +18,8 @@ public static class TestConsoleHelper
/// <summary>
/// Reads all text written to the console's standard output.
/// </summary>
/// <param name="console">The fake console instance.</param>
/// <returns>The console output text.</returns>
public static string GetOutput(FakeInMemoryConsole console)
{
console.Output.Flush();
@@ -27,6 +29,8 @@ public static class TestConsoleHelper
/// <summary>
/// Reads all text written to the console's standard error.
/// </summary>
/// <param name="console">The fake console instance.</param>
/// <returns>The console error text.</returns>
public static string GetError(FakeInMemoryConsole console)
{
console.Error.Flush();
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class WriteCommandTests
{
/// <summary>Verifies that write command executes successfully.</summary>
[Fact]
public async Task Execute_WritesSuccessfully()
{
@@ -31,6 +32,7 @@ public class WriteCommandTests
output.ShouldContain("Write successful: ns=2;s=MyVar = 100");
}
/// <summary>Verifies that write command reports failure.</summary>
[Fact]
public async Task Execute_ReportsFailure()
{
@@ -54,6 +56,7 @@ public class WriteCommandTests
output.ShouldContain("Write failed:");
}
/// <summary>Verifies that write command reads current value before writing.</summary>
[Fact]
public async Task Execute_ReadsCurrentValueThenWrites()
{
@@ -79,6 +82,7 @@ public class WriteCommandTests
fakeService.WriteValues[0].Value.ShouldBeOfType<double>();
}
/// <summary>Verifies that write command disconnects in finally block.</summary>
[Fact]
public async Task Execute_DisconnectsInFinally()
{
@@ -7,6 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests;
[Trait("Category", "Unit")]
public sealed class ClientStoragePathsTests
{
/// <summary>Verifies that GetRoot returns the canonical folder name under LocalAppData.</summary>
[Fact]
public void GetRoot_ReturnsCanonicalFolderName_UnderLocalAppData()
{
@@ -15,6 +16,7 @@ public sealed class ClientStoragePathsTests
root.ShouldContain(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));
}
/// <summary>Verifies that GetPkiPath nests PKI under root.</summary>
[Fact]
public void GetPkiPath_NestsPkiUnderRoot()
{
@@ -22,12 +24,14 @@ public sealed class ClientStoragePathsTests
pki.ShouldEndWith(Path.Combine(ClientStoragePaths.CanonicalFolderName, "pki"));
}
/// <summary>Verifies that CanonicalFolderName is OtOpcUaClient.</summary>
[Fact]
public void CanonicalFolderName_IsOtOpcUaClient()
{
ClientStoragePaths.CanonicalFolderName.ShouldBe("OtOpcUaClient");
}
/// <summary>Verifies that LegacyFolderName is LmxOpcUaClient.</summary>
[Fact]
public void LegacyFolderName_IsLmxOpcUaClient()
{
@@ -36,6 +40,7 @@ public sealed class ClientStoragePathsTests
ClientStoragePaths.LegacyFolderName.ShouldBe("LmxOpcUaClient");
}
/// <summary>Verifies that TryRunLegacyMigration returns false on repeat invocation.</summary>
[Fact]
public void TryRunLegacyMigration_Returns_False_On_Repeat_Invocation()
{
@@ -6,10 +6,16 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Fakes;
internal sealed class FakeApplicationConfigurationFactory : IApplicationConfigurationFactory
{
/// <summary>Gets or sets a value indicating whether to throw when Create is called.</summary>
public bool ThrowOnCreate { get; set; }
/// <summary>Gets the number of times CreateAsync has been called.</summary>
public int CreateCallCount { get; private set; }
/// <summary>Gets the last connection settings passed to CreateAsync.</summary>
public ConnectionSettings? LastSettings { get; private set; }
/// <inheritdoc />
public Task<ApplicationConfiguration> CreateAsync(ConnectionSettings settings, CancellationToken ct)
{
CreateCallCount++;
@@ -5,10 +5,18 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Fakes;
internal sealed class FakeEndpointDiscovery : IEndpointDiscovery
{
/// <summary>Gets or sets a value indicating whether SelectEndpoint should throw an exception.</summary>
public bool ThrowOnSelect { get; set; }
/// <summary>Gets the number of times SelectEndpoint has been called.</summary>
public int SelectCallCount { get; private set; }
/// <summary>Gets the last endpoint URL passed to SelectEndpoint.</summary>
public string? LastEndpointUrl { get; private set; }
/// <summary>Selects an endpoint for the given configuration and URL, optionally throwing an exception.</summary>
/// <param name="config">The application configuration.</param>
/// <param name="endpointUrl">The endpoint URL to select.</param>
/// <param name="requestedMode">The requested security mode.</param>
/// <returns>The selected endpoint description.</returns>
public EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl,
MessageSecurityMode requestedMode)
{
@@ -20,31 +20,53 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
/// Gets a value indicating whether the fake session has been disposed.
/// </summary>
public bool Disposed { get; private set; }
/// <summary>Gets the number of times ReadValueAsync has been called.</summary>
public int ReadCount { get; private set; }
/// <summary>Gets the number of times WriteValueAsync has been called.</summary>
public int WriteCount { get; private set; }
/// <summary>Gets the number of times BrowseAsync has been called.</summary>
public int BrowseCount { get; private set; }
/// <summary>Gets the number of times BrowseNextAsync has been called.</summary>
public int BrowseNextCount { get; private set; }
/// <summary>Gets the number of times HasChildrenAsync has been called.</summary>
public int HasChildrenCount { get; private set; }
/// <summary>Gets the number of times HistoryReadRawAsync has been called.</summary>
public int HistoryReadRawCount { get; private set; }
/// <summary>Gets the number of times HistoryReadAggregateAsync has been called.</summary>
public int HistoryReadAggregateCount { get; private set; }
// Configurable responses
/// <summary>Gets or sets the data value returned by read operations.</summary>
public DataValue? ReadResponse { get; set; }
/// <summary>Gets or sets a function to generate read responses dynamically based on the node ID.</summary>
public Func<NodeId, DataValue>? ReadResponseFunc { get; set; }
/// <summary>Gets or sets the status code returned by write operations.</summary>
public StatusCode WriteResponse { get; set; } = StatusCodes.Good;
/// <summary>Gets or sets a value indicating whether read operations should throw an exception.</summary>
public bool ThrowOnRead { get; set; }
/// <summary>Gets or sets a value indicating whether write operations should throw an exception.</summary>
public bool ThrowOnWrite { get; set; }
/// <summary>Gets or sets a value indicating whether browse operations should throw an exception.</summary>
public bool ThrowOnBrowse { get; set; }
/// <summary>Gets or sets the browse references returned by browse operations.</summary>
public ReferenceDescriptionCollection BrowseResponse { get; set; } = [];
/// <summary>Gets or sets the continuation point for browse operations.</summary>
public byte[]? BrowseContinuationPoint { get; set; }
/// <summary>Gets or sets the browse references returned by browse-next operations.</summary>
public ReferenceDescriptionCollection BrowseNextResponse { get; set; } = [];
/// <summary>Gets or sets the continuation point for browse-next operations.</summary>
public byte[]? BrowseNextContinuationPoint { get; set; }
/// <summary>Gets or sets the result returned by has-children operations.</summary>
public bool HasChildrenResponse { get; set; } = false;
/// <summary>Gets or sets the historical values returned by raw history-read operations.</summary>
public List<DataValue> HistoryReadRawResponse { get; set; } = [];
/// <summary>Gets or sets the historical values returned by aggregate history-read operations.</summary>
public List<DataValue> HistoryReadAggregateResponse { get; set; } = [];
/// <summary>Gets or sets a value indicating whether raw history-read operations should throw an exception.</summary>
public bool ThrowOnHistoryReadRaw { get; set; }
/// <summary>Gets or sets a value indicating whether aggregate history-read operations should throw an exception.</summary>
public bool ThrowOnHistoryReadAggregate { get; set; }
/// <summary>
@@ -210,6 +232,7 @@ internal sealed class FakeSessionAdapter : ISessionAdapter
/// <summary>
/// Simulates a keep-alive event.
/// </summary>
/// <param name="isGood">Whether the keep-alive status is good.</param>
public void SimulateKeepAlive(bool isGood)
{
_keepAliveCallback?.Invoke(isGood);
@@ -8,8 +8,11 @@ internal sealed class FakeSessionFactory : ISessionFactory
private readonly List<FakeSessionAdapter> _createdSessions = [];
private readonly Queue<FakeSessionAdapter> _sessions = new();
/// <summary>Gets the number of times CreateSessionAsync has been called.</summary>
public int CreateCallCount { get; private set; }
/// <summary>Gets or sets a value indicating whether CreateSessionAsync should throw.</summary>
public bool ThrowOnCreate { get; set; }
/// <summary>Gets the endpoint URL from the last CreateSessionAsync call.</summary>
public string? LastEndpointUrl { get; private set; }
/// <summary>
@@ -18,8 +21,17 @@ internal sealed class FakeSessionFactory : ISessionFactory
/// </summary>
public TaskCompletionSource? CreateGate { get; set; }
/// <summary>Gets the list of sessions created via CreateSessionAsync.</summary>
public IReadOnlyList<FakeSessionAdapter> CreatedSessions => _createdSessions;
/// <summary>Creates a session asynchronously (fake implementation).</summary>
/// <param name="config">The application configuration.</param>
/// <param name="endpoint">The endpoint description.</param>
/// <param name="sessionName">The session name.</param>
/// <param name="sessionTimeoutMs">The session timeout in milliseconds.</param>
/// <param name="identity">The user identity.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task containing the session adapter.</returns>
public async Task<ISessionAdapter> CreateSessionAsync(
ApplicationConfiguration config, EndpointDescription endpoint, string sessionName,
uint sessionTimeoutMs, UserIdentity identity, CancellationToken ct)
@@ -54,6 +66,7 @@ internal sealed class FakeSessionFactory : ISessionFactory
/// <summary>
/// Enqueues a session adapter to be returned on the next call to CreateSessionAsync.
/// </summary>
/// <param name="session">The session adapter to enqueue.</param>
public void EnqueueSession(FakeSessionAdapter session)
{
_sessions.Enqueue(session);
@@ -31,8 +31,13 @@ internal sealed class FakeSubscriptionAdapter : ISubscriptionAdapter
/// Gets or sets a value indicating whether condition refresh should throw to simulate unsupported servers.
/// </summary>
public bool ThrowOnConditionRefresh { get; set; }
/// <summary>Gets the number of times AddDataChangeMonitoredItemAsync was called.</summary>
public int AddDataChangeCount { get; private set; }
/// <summary>Gets the number of times AddEventMonitoredItemAsync was called.</summary>
public int AddEventCount { get; private set; }
/// <summary>Gets the number of times RemoveMonitoredItemAsync was called.</summary>
public int RemoveCount { get; private set; }
/// <summary>
@@ -115,6 +120,8 @@ internal sealed class FakeSubscriptionAdapter : ISubscriptionAdapter
/// <summary>
/// Simulates a data change notification for testing.
/// </summary>
/// <param name="handle">The monitored item handle.</param>
/// <param name="value">The new data value to simulate.</param>
public void SimulateDataChange(uint handle, DataValue value)
{
(NodeId NodeId, Action<string, DataValue>? DataCallback, Action<EventFieldList>? EventCallback) item;
@@ -129,6 +136,8 @@ internal sealed class FakeSubscriptionAdapter : ISubscriptionAdapter
/// <summary>
/// Simulates an event notification for testing.
/// </summary>
/// <param name="handle">The monitored item handle.</param>
/// <param name="eventFields">The event field list to simulate.</param>
public void SimulateEvent(uint handle, EventFieldList eventFields)
{
(NodeId NodeId, Action<string, DataValue>? DataCallback, Action<EventFieldList>? EventCallback) item;
@@ -8,6 +8,8 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Helpers;
public class AggregateTypeMapperTests
{
/// <summary>Verifies that ToNodeId returns a non-null NodeId for all AggregateType values.</summary>
/// <param name="aggregate">The aggregate type to test.</param>
[Theory]
[InlineData(AggregateType.Average)]
[InlineData(AggregateType.Minimum)]
@@ -23,42 +25,49 @@ public class AggregateTypeMapperTests
nodeId.IsNullNodeId.ShouldBeFalse();
}
/// <summary>Verifies that Average aggregate type maps to the correct OPC UA node ID.</summary>
[Fact]
public void ToNodeId_Average_MapsCorrectly()
{
AggregateTypeMapper.ToNodeId(AggregateType.Average).ShouldBe(ObjectIds.AggregateFunction_Average);
}
/// <summary>Verifies that Minimum aggregate type maps to the correct OPC UA node ID.</summary>
[Fact]
public void ToNodeId_Minimum_MapsCorrectly()
{
AggregateTypeMapper.ToNodeId(AggregateType.Minimum).ShouldBe(ObjectIds.AggregateFunction_Minimum);
}
/// <summary>Verifies that Maximum aggregate type maps to the correct OPC UA node ID.</summary>
[Fact]
public void ToNodeId_Maximum_MapsCorrectly()
{
AggregateTypeMapper.ToNodeId(AggregateType.Maximum).ShouldBe(ObjectIds.AggregateFunction_Maximum);
}
/// <summary>Verifies that Count aggregate type maps to the correct OPC UA node ID.</summary>
[Fact]
public void ToNodeId_Count_MapsCorrectly()
{
AggregateTypeMapper.ToNodeId(AggregateType.Count).ShouldBe(ObjectIds.AggregateFunction_Count);
}
/// <summary>Verifies that Start aggregate type maps to the correct OPC UA node ID.</summary>
[Fact]
public void ToNodeId_Start_MapsCorrectly()
{
AggregateTypeMapper.ToNodeId(AggregateType.Start).ShouldBe(ObjectIds.AggregateFunction_Start);
}
/// <summary>Verifies that End aggregate type maps to the correct OPC UA node ID.</summary>
[Fact]
public void ToNodeId_End_MapsCorrectly()
{
AggregateTypeMapper.ToNodeId(AggregateType.End).ShouldBe(ObjectIds.AggregateFunction_End);
}
/// <summary>Verifies that StandardDeviation aggregate type maps to the correct OPC UA node ID.</summary>
[Fact]
public void ToNodeId_StandardDeviation_MapsCorrectly()
{
@@ -66,6 +75,7 @@ public class AggregateTypeMapperTests
.ShouldBe(ObjectIds.AggregateFunction_StandardDeviationPopulation);
}
/// <summary>Verifies that invalid aggregate type value throws ArgumentOutOfRangeException.</summary>
[Fact]
public void ToNodeId_InvalidValue_Throws()
{
@@ -6,6 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Helpers;
public class FailoverUrlParserTests
{
/// <summary>Verifies that Parse returns only the primary URL when CSV failover list is null.</summary>
[Fact]
public void Parse_CsvNull_ReturnsPrimaryOnly()
{
@@ -13,6 +14,7 @@ public class FailoverUrlParserTests
result.ShouldBe(["opc.tcp://primary:4840"]);
}
/// <summary>Verifies that Parse returns only the primary URL when CSV failover list is empty.</summary>
[Fact]
public void Parse_CsvEmpty_ReturnsPrimaryOnly()
{
@@ -20,6 +22,7 @@ public class FailoverUrlParserTests
result.ShouldBe(["opc.tcp://primary:4840"]);
}
/// <summary>Verifies that Parse returns only the primary URL when CSV failover list is whitespace.</summary>
[Fact]
public void Parse_CsvWhitespace_ReturnsPrimaryOnly()
{
@@ -27,6 +30,7 @@ public class FailoverUrlParserTests
result.ShouldBe(["opc.tcp://primary:4840"]);
}
/// <summary>Verifies that Parse returns both primary and single failover URL.</summary>
[Fact]
public void Parse_SingleFailover_ReturnsBoth()
{
@@ -34,6 +38,7 @@ public class FailoverUrlParserTests
result.ShouldBe(["opc.tcp://primary:4840", "opc.tcp://backup:4840"]);
}
/// <summary>Verifies that Parse returns primary URL and all failover URLs from CSV list.</summary>
[Fact]
public void Parse_MultipleFailovers_ReturnsAll()
{
@@ -41,6 +46,7 @@ public class FailoverUrlParserTests
result.ShouldBe(["opc.tcp://primary:4840", "opc.tcp://backup1:4840", "opc.tcp://backup2:4840"]);
}
/// <summary>Verifies that Parse trims leading and trailing whitespace from failover URLs.</summary>
[Fact]
public void Parse_TrimsWhitespace()
{
@@ -48,6 +54,7 @@ public class FailoverUrlParserTests
result.ShouldBe(["opc.tcp://primary:4840", "opc.tcp://backup:4840"]);
}
/// <summary>Verifies that Parse deduplicates the primary URL if present in the failover list.</summary>
[Fact]
public void Parse_DeduplicatesPrimaryInFailoverList()
{
@@ -55,6 +62,7 @@ public class FailoverUrlParserTests
result.ShouldBe(["opc.tcp://primary:4840", "opc.tcp://backup:4840"]);
}
/// <summary>Verifies that Parse performs case-insensitive deduplication of URLs.</summary>
[Fact]
public void Parse_DeduplicatesCaseInsensitive()
{
@@ -62,6 +70,7 @@ public class FailoverUrlParserTests
result.ShouldBe(["opc.tcp://Primary:4840"]);
}
/// <summary>Verifies that Parse returns only the primary URL when array of failover URLs is null.</summary>
[Fact]
public void Parse_ArrayNull_ReturnsPrimaryOnly()
{
@@ -69,6 +78,7 @@ public class FailoverUrlParserTests
result.ShouldBe(["opc.tcp://primary:4840"]);
}
/// <summary>Verifies that Parse returns only the primary URL when array of failover URLs is empty.</summary>
[Fact]
public void Parse_ArrayEmpty_ReturnsPrimaryOnly()
{
@@ -76,6 +86,7 @@ public class FailoverUrlParserTests
result.ShouldBe(["opc.tcp://primary:4840"]);
}
/// <summary>Verifies that Parse returns primary URL and all failover URLs from array.</summary>
[Fact]
public void Parse_ArrayWithUrls_ReturnsAll()
{
@@ -84,6 +95,7 @@ public class FailoverUrlParserTests
result.ShouldBe(["opc.tcp://primary:4840", "opc.tcp://backup1:4840", "opc.tcp://backup2:4840"]);
}
/// <summary>Verifies that Parse deduplicates URLs from array if primary URL is present.</summary>
[Fact]
public void Parse_ArrayDeduplicates()
{
@@ -92,6 +104,7 @@ public class FailoverUrlParserTests
result.ShouldBe(["opc.tcp://primary:4840", "opc.tcp://backup:4840"]);
}
/// <summary>Verifies that Parse trims whitespace from URLs in array.</summary>
[Fact]
public void Parse_ArrayTrimsWhitespace()
{
@@ -100,6 +113,7 @@ public class FailoverUrlParserTests
result.ShouldBe(["opc.tcp://primary:4840", "opc.tcp://backup:4840"]);
}
/// <summary>Verifies that Parse skips null and empty URLs from array.</summary>
[Fact]
public void Parse_ArraySkipsNullAndEmpty()
{
@@ -8,6 +8,9 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Helpers;
public class SecurityModeMapperTests
{
/// <summary>Verifies ToMessageSecurityMode correctly maps SecurityMode values to OPC UA MessageSecurityMode.</summary>
/// <param name="input">The SecurityMode value to map.</param>
/// <param name="expected">The expected MessageSecurityMode result.</param>
[Theory]
[InlineData(SecurityMode.None, MessageSecurityMode.None)]
[InlineData(SecurityMode.Sign, MessageSecurityMode.Sign)]
@@ -17,6 +20,7 @@ public class SecurityModeMapperTests
SecurityModeMapper.ToMessageSecurityMode(input).ShouldBe(expected);
}
/// <summary>Verifies ToMessageSecurityMode throws on invalid SecurityMode values.</summary>
[Fact]
public void ToMessageSecurityMode_InvalidValue_Throws()
{
@@ -24,6 +28,9 @@ public class SecurityModeMapperTests
SecurityModeMapper.ToMessageSecurityMode((SecurityMode)99));
}
/// <summary>Verifies FromString correctly parses security mode strings (case-insensitive).</summary>
/// <param name="input">The security mode string to parse.</param>
/// <param name="expected">The expected SecurityMode result.</param>
[Theory]
[InlineData("none", SecurityMode.None)]
[InlineData("None", SecurityMode.None)]
@@ -38,18 +45,21 @@ public class SecurityModeMapperTests
SecurityModeMapper.FromString(input).ShouldBe(expected);
}
/// <summary>Verifies FromString correctly parses strings with leading and trailing whitespace.</summary>
[Fact]
public void FromString_WithWhitespace_ParsesCorrectly()
{
SecurityModeMapper.FromString(" sign ").ShouldBe(SecurityMode.Sign);
}
/// <summary>Verifies FromString throws on unrecognized security mode strings.</summary>
[Fact]
public void FromString_UnknownValue_Throws()
{
Should.Throw<ArgumentException>(() => SecurityModeMapper.FromString("invalid"));
}
/// <summary>Verifies FromString returns None when passed a null string.</summary>
[Fact]
public void FromString_Null_DefaultsToNone()
{
@@ -6,96 +6,112 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Helpers;
public class ValueConverterTests
{
/// <summary>Verifies that a boolean string "True" is converted to true.</summary>
[Fact]
public void ConvertValue_Bool_True()
{
ValueConverter.ConvertValue("True", true).ShouldBe(true);
}
/// <summary>Verifies that a boolean string "False" is converted to false.</summary>
[Fact]
public void ConvertValue_Bool_False()
{
ValueConverter.ConvertValue("False", false).ShouldBe(false);
}
/// <summary>Verifies that a byte value is converted correctly.</summary>
[Fact]
public void ConvertValue_Byte()
{
ValueConverter.ConvertValue("255", (byte)0).ShouldBe((byte)255);
}
/// <summary>Verifies that a short value is converted correctly.</summary>
[Fact]
public void ConvertValue_Short()
{
ValueConverter.ConvertValue("-100", (short)0).ShouldBe((short)-100);
}
/// <summary>Verifies that an unsigned short value is converted correctly.</summary>
[Fact]
public void ConvertValue_UShort()
{
ValueConverter.ConvertValue("65535", (ushort)0).ShouldBe((ushort)65535);
}
/// <summary>Verifies that an integer value is converted correctly.</summary>
[Fact]
public void ConvertValue_Int()
{
ValueConverter.ConvertValue("42", 0).ShouldBe(42);
}
/// <summary>Verifies that an unsigned integer value is converted correctly.</summary>
[Fact]
public void ConvertValue_UInt()
{
ValueConverter.ConvertValue("42", 0u).ShouldBe(42u);
}
/// <summary>Verifies that a long value is converted correctly.</summary>
[Fact]
public void ConvertValue_Long()
{
ValueConverter.ConvertValue("9999999999", 0L).ShouldBe(9999999999L);
}
/// <summary>Verifies that an unsigned long value is converted correctly.</summary>
[Fact]
public void ConvertValue_ULong()
{
ValueConverter.ConvertValue("18446744073709551615", 0UL).ShouldBe(ulong.MaxValue);
}
/// <summary>Verifies that a float value is converted correctly.</summary>
[Fact]
public void ConvertValue_Float()
{
ValueConverter.ConvertValue("3.14", 0f).ShouldBe(3.14f);
}
/// <summary>Verifies that a double value is converted correctly.</summary>
[Fact]
public void ConvertValue_Double()
{
ValueConverter.ConvertValue("3.14159", 0.0).ShouldBe(3.14159);
}
/// <summary>Verifies that a string value is converted correctly when the current value is a string.</summary>
[Fact]
public void ConvertValue_String_WhenCurrentIsString()
{
ValueConverter.ConvertValue("hello", "").ShouldBe("hello");
}
/// <summary>Verifies that a string value is converted correctly when the current value is null.</summary>
[Fact]
public void ConvertValue_String_WhenCurrentIsNull()
{
ValueConverter.ConvertValue("hello", null).ShouldBe("hello");
}
/// <summary>Verifies that a string value is converted correctly when the current value is an unknown type.</summary>
[Fact]
public void ConvertValue_String_WhenCurrentIsUnknownType()
{
ValueConverter.ConvertValue("hello", new object()).ShouldBe("hello");
}
/// <summary>Verifies that converting an invalid boolean value throws a FormatException.</summary>
[Fact]
public void ConvertValue_InvalidBool_Throws()
{
Should.Throw<FormatException>(() => ValueConverter.ConvertValue("notabool", true));
}
/// <summary>Verifies that converting an invalid integer value throws a FormatException with a descriptive message.</summary>
[Fact]
public void ConvertValue_InvalidInt_ThrowsWithDescription()
{
@@ -104,6 +120,7 @@ public class ValueConverterTests
ex.Message.ShouldContain("notanint");
}
/// <summary>Verifies that an overflow during conversion throws a FormatException.</summary>
[Fact]
public void ConvertValue_Overflow_ThrowsFormatException()
{
@@ -114,6 +131,9 @@ public class ValueConverterTests
// --- Client.Shared-008: Boolean aliases ---
/// <summary>Verifies that boolean values accept numeric and word aliases.</summary>
/// <param name="input">The input string value.</param>
/// <param name="expected">The expected boolean value.</param>
[Theory]
[InlineData("1", true)]
[InlineData("0", false)]
@@ -130,6 +150,7 @@ public class ValueConverterTests
ValueConverter.ConvertValue(input, true).ShouldBe(expected);
}
/// <summary>Verifies that converting an invalid boolean value throws a descriptive FormatException.</summary>
[Fact]
public void ConvertValue_InvalidBool_ThrowsDescriptiveFormatException()
{
@@ -6,6 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Models;
public class ConnectionSettingsTests
{
/// <summary>Verifies that ConnectionSettings defaults are correct.</summary>
[Fact]
public void Defaults_AreCorrect()
{
@@ -24,6 +25,7 @@ public class ConnectionSettingsTests
settings.CertificateStorePath.ShouldBe(string.Empty);
}
/// <summary>Verifies that validation throws on null endpoint URL.</summary>
[Fact]
public void Validate_ThrowsOnNullEndpointUrl()
{
@@ -32,6 +34,7 @@ public class ConnectionSettingsTests
.ParamName.ShouldBe("EndpointUrl");
}
/// <summary>Verifies that validation throws on empty endpoint URL.</summary>
[Fact]
public void Validate_ThrowsOnEmptyEndpointUrl()
{
@@ -40,6 +43,7 @@ public class ConnectionSettingsTests
.ParamName.ShouldBe("EndpointUrl");
}
/// <summary>Verifies that validation throws on whitespace endpoint URL.</summary>
[Fact]
public void Validate_ThrowsOnWhitespaceEndpointUrl()
{
@@ -48,6 +52,7 @@ public class ConnectionSettingsTests
.ParamName.ShouldBe("EndpointUrl");
}
/// <summary>Verifies that validation throws on zero timeout.</summary>
[Fact]
public void Validate_ThrowsOnZeroTimeout()
{
@@ -60,6 +65,7 @@ public class ConnectionSettingsTests
.ParamName.ShouldBe("SessionTimeoutSeconds");
}
/// <summary>Verifies that validation throws on negative timeout.</summary>
[Fact]
public void Validate_ThrowsOnNegativeTimeout()
{
@@ -72,6 +78,7 @@ public class ConnectionSettingsTests
.ParamName.ShouldBe("SessionTimeoutSeconds");
}
/// <summary>Verifies that validation throws on timeout above 3600 seconds.</summary>
[Fact]
public void Validate_ThrowsOnTimeoutAbove3600()
{
@@ -84,6 +91,7 @@ public class ConnectionSettingsTests
.ParamName.ShouldBe("SessionTimeoutSeconds");
}
/// <summary>Verifies that validation succeeds with valid settings.</summary>
[Fact]
public void Validate_SucceedsWithValidSettings()
{
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Models;
public class ModelConstructionTests
{
/// <summary>Verifies that BrowseResult constructs correctly with all properties.</summary>
[Fact]
public void BrowseResult_ConstructsCorrectly()
{
@@ -19,6 +20,7 @@ public class ModelConstructionTests
result.HasChildren.ShouldBeTrue();
}
/// <summary>Verifies that BrowseResult correctly sets HasChildren to false.</summary>
[Fact]
public void BrowseResult_WithoutChildren()
{
@@ -26,6 +28,7 @@ public class ModelConstructionTests
result.HasChildren.ShouldBeFalse();
}
/// <summary>Verifies that AlarmEventArgs constructs correctly with all properties.</summary>
[Fact]
public void AlarmEventArgs_ConstructsCorrectly()
{
@@ -42,6 +45,7 @@ public class ModelConstructionTests
args.Time.ShouldBe(time);
}
/// <summary>Verifies that RedundancyInfo constructs correctly with all properties.</summary>
[Fact]
public void RedundancyInfo_ConstructsCorrectly()
{
@@ -54,6 +58,7 @@ public class ModelConstructionTests
info.ApplicationUri.ShouldBe("urn:server1");
}
/// <summary>Verifies that RedundancyInfo handles empty URIs correctly.</summary>
[Fact]
public void RedundancyInfo_WithEmptyUris()
{
@@ -62,6 +67,7 @@ public class ModelConstructionTests
info.ApplicationUri.ShouldBeEmpty();
}
/// <summary>Verifies that DataChangedEventArgs constructs correctly with all properties.</summary>
[Fact]
public void DataChangedEventArgs_ConstructsCorrectly()
{
@@ -73,6 +79,7 @@ public class ModelConstructionTests
args.Value.Value.ShouldBe(42);
}
/// <summary>Verifies that ConnectionStateChangedEventArgs constructs correctly with all properties.</summary>
[Fact]
public void ConnectionStateChangedEventArgs_ConstructsCorrectly()
{
@@ -84,6 +91,7 @@ public class ModelConstructionTests
args.EndpointUrl.ShouldBe("opc.tcp://localhost:4840");
}
/// <summary>Verifies that ConnectionInfo constructs correctly with all properties.</summary>
[Fact]
public void ConnectionInfo_ConstructsCorrectly()
{
@@ -103,6 +111,7 @@ public class ModelConstructionTests
info.SessionName.ShouldBe("TestSession");
}
/// <summary>Verifies that SecurityMode enum has the expected number of values.</summary>
[Fact]
public void SecurityMode_Enum_HasExpectedValues()
{
@@ -112,6 +121,7 @@ public class ModelConstructionTests
((int)SecurityMode.SignAndEncrypt).ShouldBe(2);
}
/// <summary>Verifies that ConnectionState enum has the expected number of values.</summary>
[Fact]
public void ConnectionState_Enum_HasExpectedValues()
{
@@ -122,6 +132,7 @@ public class ModelConstructionTests
((int)ConnectionState.Reconnecting).ShouldBe(3);
}
/// <summary>Verifies that AggregateType enum has the expected number of values.</summary>
[Fact]
public void AggregateType_Enum_HasExpectedValues()
{
@@ -16,6 +16,7 @@ public class OpcUaClientServiceTests : IDisposable
private readonly OpcUaClientService _service;
private readonly FakeSessionFactory _sessionFactory = new();
/// <summary>Initializes a new test instance with fake dependencies.</summary>
public OpcUaClientServiceTests()
{
_service = new OpcUaClientService(_configFactory, _endpointDiscovery, _sessionFactory);
@@ -12,6 +12,7 @@ public class AlarmsViewModelTests
private readonly FakeOpcUaClientService _service;
private readonly AlarmsViewModel _vm;
/// <summary>Initializes a new test instance.</summary>
public AlarmsViewModelTests()
{
_service = new FakeOpcUaClientService();
@@ -19,6 +20,7 @@ public class AlarmsViewModelTests
_vm = new AlarmsViewModel(_service, dispatcher);
}
/// <summary>Verifies that SubscribeCommand cannot execute when disconnected.</summary>
[Fact]
public void SubscribeCommand_CannotExecute_WhenDisconnected()
{
@@ -26,6 +28,7 @@ public class AlarmsViewModelTests
_vm.SubscribeCommand.CanExecute(null).ShouldBeFalse();
}
/// <summary>Verifies that SubscribeCommand cannot execute when already subscribed.</summary>
[Fact]
public void SubscribeCommand_CannotExecute_WhenAlreadySubscribed()
{
@@ -34,6 +37,7 @@ public class AlarmsViewModelTests
_vm.SubscribeCommand.CanExecute(null).ShouldBeFalse();
}
/// <summary>Verifies that SubscribeCommand can execute when connected and not subscribed.</summary>
[Fact]
public void SubscribeCommand_CanExecute_WhenConnectedAndNotSubscribed()
{
@@ -42,6 +46,7 @@ public class AlarmsViewModelTests
_vm.SubscribeCommand.CanExecute(null).ShouldBeTrue();
}
/// <summary>Verifies that SubscribeCommand sets IsSubscribed flag.</summary>
[Fact]
public async Task SubscribeCommand_SetsIsSubscribed()
{
@@ -53,6 +58,7 @@ public class AlarmsViewModelTests
_service.SubscribeAlarmsCallCount.ShouldBe(1);
}
/// <summary>Verifies that UnsubscribeCommand cannot execute when not subscribed.</summary>
[Fact]
public void UnsubscribeCommand_CannotExecute_WhenNotSubscribed()
{
@@ -61,6 +67,7 @@ public class AlarmsViewModelTests
_vm.UnsubscribeCommand.CanExecute(null).ShouldBeFalse();
}
/// <summary>Verifies that UnsubscribeCommand clears IsSubscribed flag.</summary>
[Fact]
public async Task UnsubscribeCommand_ClearsIsSubscribed()
{
@@ -73,6 +80,7 @@ public class AlarmsViewModelTests
_service.UnsubscribeAlarmsCallCount.ShouldBe(1);
}
/// <summary>Verifies that RefreshCommand calls the service.</summary>
[Fact]
public async Task RefreshCommand_CallsService()
{
@@ -84,6 +92,7 @@ public class AlarmsViewModelTests
_service.RequestConditionRefreshCallCount.ShouldBe(1);
}
/// <summary>Verifies that RefreshCommand cannot execute when not subscribed.</summary>
[Fact]
public void RefreshCommand_CannotExecute_WhenNotSubscribed()
{
@@ -92,6 +101,7 @@ public class AlarmsViewModelTests
_vm.RefreshCommand.CanExecute(null).ShouldBeFalse();
}
/// <summary>Verifies that alarm events are added to the collection.</summary>
[Fact]
public void AlarmEvent_AddsToCollection()
{
@@ -108,6 +118,7 @@ public class AlarmsViewModelTests
_vm.AlarmEvents[0].Message.ShouldBe("Temperature high");
}
/// <summary>Verifies that Clear resets the view model state.</summary>
[Fact]
public void Clear_ResetsState()
{
@@ -120,6 +131,7 @@ public class AlarmsViewModelTests
_vm.IsSubscribed.ShouldBeFalse();
}
/// <summary>Verifies that Teardown unregisters the event handler.</summary>
[Fact]
public void Teardown_UnhooksEventHandler()
{
@@ -133,6 +145,7 @@ public class AlarmsViewModelTests
_vm.AlarmEvents.ShouldBeEmpty();
}
/// <summary>Verifies that the default polling interval is 1000ms.</summary>
[Fact]
public void DefaultInterval_Is1000()
{
@@ -7,12 +7,14 @@ using BrowseResult = ZB.MOM.WW.OtOpcUa.Client.Shared.Models.BrowseResult;
namespace ZB.MOM.WW.OtOpcUa.Client.UI.Tests;
/// <summary>Tests for the BrowseTreeViewModel class.</summary>
public class BrowseTreeViewModelTests
{
private readonly SynchronousUiDispatcher _dispatcher;
private readonly FakeOpcUaClientService _service;
private readonly BrowseTreeViewModel _vm;
/// <summary>Initializes a new instance of the BrowseTreeViewModelTests class.</summary>
public BrowseTreeViewModelTests()
{
_service = new FakeOpcUaClientService
@@ -27,6 +29,7 @@ public class BrowseTreeViewModelTests
_vm = new BrowseTreeViewModel(_service, _dispatcher);
}
/// <summary>Verifies that LoadRootsAsync populates root nodes.</summary>
[Fact]
public async Task LoadRootsAsync_PopulatesRootNodes()
{
@@ -37,6 +40,7 @@ public class BrowseTreeViewModelTests
_vm.RootNodes[1].DisplayName.ShouldBe("Node2");
}
/// <summary>Verifies that LoadRootsAsync browses with null parent.</summary>
[Fact]
public async Task LoadRootsAsync_BrowsesWithNullParent()
{
@@ -46,6 +50,7 @@ public class BrowseTreeViewModelTests
_service.LastBrowseParentNodeId.ShouldBeNull();
}
/// <summary>Verifies that Clear removes all root nodes.</summary>
[Fact]
public void Clear_RemovesAllRootNodes()
{
@@ -55,6 +60,7 @@ public class BrowseTreeViewModelTests
_vm.RootNodes.ShouldBeEmpty();
}
/// <summary>Verifies that nodes with children have a placeholder.</summary>
[Fact]
public async Task LoadRootsAsync_NodeWithChildren_HasPlaceholder()
{
@@ -66,6 +72,7 @@ public class BrowseTreeViewModelTests
nodeWithChildren.Children[0].IsPlaceholder.ShouldBeTrue();
}
/// <summary>Verifies that nodes without children have no placeholder.</summary>
[Fact]
public async Task LoadRootsAsync_NodeWithoutChildren_HasNoPlaceholder()
{
@@ -76,6 +83,7 @@ public class BrowseTreeViewModelTests
leafNode.Children.ShouldBeEmpty();
}
/// <summary>Verifies that first tree node expand triggers child browse.</summary>
[Fact]
public async Task TreeNode_FirstExpand_TriggersChildBrowse()
{
@@ -105,6 +113,7 @@ public class BrowseTreeViewModelTests
parent.Children[0].DisplayName.ShouldBe("Child1");
}
/// <summary>Verifies that second tree node expand does not browse again.</summary>
[Fact]
public async Task TreeNode_SecondExpand_DoesNotBrowseAgain()
{
@@ -133,6 +142,7 @@ public class BrowseTreeViewModelTests
_service.BrowseCallCount.ShouldBe(browseCountAfterFirst);
}
/// <summary>Verifies that IsLoading transitions during browse.</summary>
[Fact]
public async Task TreeNode_IsLoading_TransitionsDuringBrowse()
{
@@ -84,28 +84,50 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
public Exception? HistoryException { get; set; }
// Call tracking
/// <summary>Gets the number of times ConnectAsync has been called.</summary>
public int ConnectCallCount { get; private set; }
/// <summary>Gets the number of times DisconnectAsync has been called.</summary>
public int DisconnectCallCount { get; private set; }
/// <summary>Gets the number of times ReadValueAsync has been called.</summary>
public int ReadCallCount { get; private set; }
/// <summary>Gets the number of times WriteValueAsync has been called.</summary>
public int WriteCallCount { get; private set; }
/// <summary>Gets the number of times BrowseAsync has been called.</summary>
public int BrowseCallCount { get; private set; }
/// <summary>Gets the number of times SubscribeAsync has been called.</summary>
public int SubscribeCallCount { get; private set; }
/// <summary>Gets the number of times UnsubscribeAsync has been called.</summary>
public int UnsubscribeCallCount { get; private set; }
/// <summary>Gets the number of times SubscribeAlarmsAsync has been called.</summary>
public int SubscribeAlarmsCallCount { get; private set; }
/// <summary>Gets the number of times UnsubscribeAlarmsAsync has been called.</summary>
public int UnsubscribeAlarmsCallCount { get; private set; }
/// <summary>Gets the number of times RequestConditionRefreshAsync has been called.</summary>
public int RequestConditionRefreshCallCount { get; private set; }
/// <summary>Gets the number of times HistoryReadRawAsync has been called.</summary>
public int HistoryReadRawCallCount { get; private set; }
/// <summary>Gets the number of times HistoryReadAggregateAsync has been called.</summary>
public int HistoryReadAggregateCallCount { get; private set; }
/// <summary>Gets the number of times GetRedundancyInfoAsync has been called.</summary>
public int GetRedundancyInfoCallCount { get; private set; }
/// <summary>Gets the connection settings from the last ConnectAsync call.</summary>
public ConnectionSettings? LastConnectionSettings { get; private set; }
/// <summary>Gets the node ID from the last ReadValueAsync call.</summary>
public NodeId? LastReadNodeId { get; private set; }
/// <summary>Gets the node ID from the last WriteValueAsync call.</summary>
public NodeId? LastWriteNodeId { get; private set; }
/// <summary>Gets the value from the last WriteValueAsync call.</summary>
public object? LastWriteValue { get; private set; }
/// <summary>Gets the parent node ID from the last BrowseAsync call.</summary>
public NodeId? LastBrowseParentNodeId { get; private set; }
/// <summary>Gets the node ID from the last SubscribeAsync call.</summary>
public NodeId? LastSubscribeNodeId { get; private set; }
/// <summary>Gets the interval in milliseconds from the last SubscribeAsync call.</summary>
public int LastSubscribeIntervalMs { get; private set; }
/// <summary>Gets the node ID from the last UnsubscribeAsync call.</summary>
public NodeId? LastUnsubscribeNodeId { get; private set; }
/// <summary>Gets the aggregate type from the last HistoryReadAggregateAsync call.</summary>
public AggregateType? LastAggregateType { get; private set; }
/// <inheritdoc />
@@ -225,8 +247,11 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
return Task.CompletedTask;
}
/// <summary>Gets or sets the status code returned by acknowledgment operations in UI tests.</summary>
public StatusCode AcknowledgeResult { get; set; } = StatusCodes.Good;
/// <summary>Gets or sets the exception thrown to simulate alarm acknowledgment failures in the UI.</summary>
public Exception? AcknowledgeException { get; set; }
/// <summary>Gets the number of times AcknowledgeAlarmAsync has been called.</summary>
public int AcknowledgeCallCount { get; private set; }
/// <inheritdoc />
@@ -276,6 +301,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
/// <summary>
/// Raises a simulated data-change notification so UI tests can validate live update handling.
/// </summary>
/// <param name="args">The data change event arguments to raise.</param>
public void RaiseDataChanged(DataChangedEventArgs args)
{
DataChanged?.Invoke(this, args);
@@ -284,6 +310,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
/// <summary>
/// Raises a simulated alarm event so UI tests can validate alarm-list behavior.
/// </summary>
/// <param name="args">The alarm event arguments to raise.</param>
public void RaiseAlarmEvent(AlarmEventArgs args)
{
AlarmEvent?.Invoke(this, args);
@@ -292,6 +319,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
/// <summary>
/// Raises a simulated connection-state transition so UI tests can validate status presentation and failover behavior.
/// </summary>
/// <param name="args">The connection state changed event arguments to raise.</param>
public void RaiseConnectionStateChanged(ConnectionStateChangedEventArgs args)
{
ConnectionStateChanged?.Invoke(this, args);
@@ -9,11 +9,15 @@ public sealed class FakeOpcUaClientServiceFactory : IOpcUaClientServiceFactory
{
private readonly FakeOpcUaClientService _service;
/// <summary>Initializes a new instance of FakeOpcUaClientServiceFactory.</summary>
/// <param name="service">The fake OPC UA client service to return.</param>
public FakeOpcUaClientServiceFactory(FakeOpcUaClientService service)
{
_service = service;
}
/// <summary>Creates an OPC UA client service instance.</summary>
/// <returns>The preconfigured fake OPC UA client service.</returns>
public IOpcUaClientService Create()
{
return _service;
@@ -4,17 +4,24 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Tests.Fakes;
public sealed class FakeSettingsService : ISettingsService
{
/// <summary>Gets or sets the settings held by this fake service.</summary>
public UserSettings Settings { get; set; } = new();
/// <summary>Gets the number of times Load has been called.</summary>
public int LoadCallCount { get; private set; }
/// <summary>Gets the number of times Save has been called.</summary>
public int SaveCallCount { get; private set; }
/// <summary>Gets the last settings that were saved.</summary>
public UserSettings? LastSaved { get; private set; }
/// <summary>Loads and returns the current settings.</summary>
public UserSettings Load()
{
LoadCallCount++;
return Settings;
}
/// <summary>Saves the specified settings.</summary>
/// <param name="settings">The settings to save.</param>
public void Save(UserSettings settings)
{
SaveCallCount++;
@@ -13,6 +13,7 @@ public class HistoryViewModelTests
private readonly FakeOpcUaClientService _service;
private readonly HistoryViewModel _vm;
/// <summary>Initializes a new instance of the <see cref="HistoryViewModelTests"/> class.</summary>
public HistoryViewModelTests()
{
_service = new FakeOpcUaClientService
@@ -31,6 +32,7 @@ public class HistoryViewModelTests
_vm = new HistoryViewModel(_service, dispatcher);
}
/// <summary>Verifies that the read history command cannot execute when disconnected.</summary>
[Fact]
public void ReadHistoryCommand_CannotExecute_WhenDisconnected()
{
@@ -39,6 +41,7 @@ public class HistoryViewModelTests
_vm.ReadHistoryCommand.CanExecute(null).ShouldBeFalse();
}
/// <summary>Verifies that the read history command cannot execute when no node is selected.</summary>
[Fact]
public void ReadHistoryCommand_CannotExecute_WhenNoNodeSelected()
{
@@ -47,6 +50,7 @@ public class HistoryViewModelTests
_vm.ReadHistoryCommand.CanExecute(null).ShouldBeFalse();
}
/// <summary>Verifies that the read history command can execute when connected and a node is selected.</summary>
[Fact]
public void ReadHistoryCommand_CanExecute_WhenConnectedAndNodeSelected()
{
@@ -55,6 +59,7 @@ public class HistoryViewModelTests
_vm.ReadHistoryCommand.CanExecute(null).ShouldBeTrue();
}
/// <summary>Verifies that a raw history read populates results correctly.</summary>
[Fact]
public async Task ReadHistoryCommand_Raw_PopulatesResults()
{
@@ -71,6 +76,7 @@ public class HistoryViewModelTests
_service.HistoryReadAggregateCallCount.ShouldBe(0);
}
/// <summary>Verifies that an aggregate history read populates results correctly.</summary>
[Fact]
public async Task ReadHistoryCommand_Aggregate_PopulatesResults()
{
@@ -87,6 +93,7 @@ public class HistoryViewModelTests
_service.HistoryReadRawCallCount.ShouldBe(0);
}
/// <summary>Verifies that the read history command clears previous results before loading new ones.</summary>
[Fact]
public async Task ReadHistoryCommand_ClearsResultsBefore()
{
@@ -99,6 +106,7 @@ public class HistoryViewModelTests
_vm.Results.ShouldNotContain(r => r.Value == "old");
}
/// <summary>Verifies that the loading state is false after the read history command completes.</summary>
[Fact]
public async Task ReadHistoryCommand_IsLoading_FalseAfterComplete()
{
@@ -110,6 +118,7 @@ public class HistoryViewModelTests
_vm.IsLoading.ShouldBeFalse();
}
/// <summary>Verifies that default values are initialized correctly.</summary>
[Fact]
public void DefaultValues_AreCorrect()
{
@@ -119,6 +128,7 @@ public class HistoryViewModelTests
_vm.IsAggregateRead.ShouldBeFalse();
}
/// <summary>Verifies that IsAggregateRead returns true when an aggregate type is selected.</summary>
[Fact]
public void IsAggregateRead_TrueWhenAggregateSelected()
{
@@ -126,6 +136,7 @@ public class HistoryViewModelTests
_vm.IsAggregateRead.ShouldBeTrue();
}
/// <summary>Verifies that the aggregate types collection contains null for raw reads.</summary>
[Fact]
public void AggregateTypes_ContainsNullForRaw()
{
@@ -133,6 +144,7 @@ public class HistoryViewModelTests
_vm.AggregateTypes.Count.ShouldBe(8); // null + 7 enum values
}
/// <summary>Verifies that the Clear method resets the view model state.</summary>
[Fact]
public void Clear_ResetsState()
{
@@ -145,6 +157,7 @@ public class HistoryViewModelTests
_vm.SelectedNodeId.ShouldBeNull();
}
/// <summary>Verifies that read history command errors are displayed in the results.</summary>
[Fact]
public async Task ReadHistoryCommand_Error_ShowsErrorInResults()
{
@@ -18,6 +18,7 @@ public class MainWindowViewModelTests
private readonly FakeSettingsService _settingsService;
private readonly MainWindowViewModel _vm;
/// <summary>Initializes test fixtures with default client and settings services.</summary>
public MainWindowViewModelTests()
{
_service = new FakeOpcUaClientService
@@ -12,6 +12,7 @@ public class ReadWriteViewModelTests
private readonly FakeOpcUaClientService _service;
private readonly ReadWriteViewModel _vm;
/// <summary>Initializes a new instance of the ReadWriteViewModelTests class.</summary>
public ReadWriteViewModelTests()
{
_service = new FakeOpcUaClientService
@@ -22,6 +23,7 @@ public class ReadWriteViewModelTests
_vm = new ReadWriteViewModel(_service, dispatcher);
}
/// <summary>Verifies that the read command cannot execute when disconnected.</summary>
[Fact]
public void ReadCommand_CannotExecute_WhenDisconnected()
{
@@ -30,6 +32,7 @@ public class ReadWriteViewModelTests
_vm.ReadCommand.CanExecute(null).ShouldBeFalse();
}
/// <summary>Verifies that the read command cannot execute when no node is selected.</summary>
[Fact]
public void ReadCommand_CannotExecute_WhenNoNodeSelected()
{
@@ -38,6 +41,7 @@ public class ReadWriteViewModelTests
_vm.ReadCommand.CanExecute(null).ShouldBeFalse();
}
/// <summary>Verifies that the read command can execute when connected and a node is selected.</summary>
[Fact]
public void ReadCommand_CanExecute_WhenConnectedAndNodeSelected()
{
@@ -46,6 +50,7 @@ public class ReadWriteViewModelTests
_vm.ReadCommand.CanExecute(null).ShouldBeTrue();
}
/// <summary>Verifies that the read command updates value and status.</summary>
[Fact]
public async Task ReadCommand_UpdatesValueAndStatus()
{
@@ -63,6 +68,7 @@ public class ReadWriteViewModelTests
(_service.ReadCallCount - countBefore).ShouldBe(1);
}
/// <summary>Verifies that auto-read fires on selection change when connected.</summary>
[Fact]
public void AutoRead_OnSelectionChange_WhenConnected()
{
@@ -74,6 +80,7 @@ public class ReadWriteViewModelTests
_service.ReadCallCount.ShouldBeGreaterThanOrEqualTo(1);
}
/// <summary>Verifies that null selection does not call the service.</summary>
[Fact]
public void NullSelection_DoesNotCallService()
{
@@ -83,6 +90,7 @@ public class ReadWriteViewModelTests
_service.ReadCallCount.ShouldBe(0);
}
/// <summary>Verifies that the write command updates write status.</summary>
[Fact]
public async Task WriteCommand_UpdatesWriteStatus()
{
@@ -100,6 +108,7 @@ public class ReadWriteViewModelTests
_service.LastWriteValue.ShouldBe("NewValue");
}
/// <summary>Verifies that the write command cannot execute when disconnected.</summary>
[Fact]
public void WriteCommand_CannotExecute_WhenDisconnected()
{
@@ -108,6 +117,7 @@ public class ReadWriteViewModelTests
_vm.WriteCommand.CanExecute(null).ShouldBeFalse();
}
/// <summary>Verifies that read command error sets error status.</summary>
[Fact]
public async Task ReadCommand_Error_SetsErrorStatus()
{
@@ -121,6 +131,7 @@ public class ReadWriteViewModelTests
_vm.CurrentStatus.ShouldContain("Error");
}
/// <summary>Verifies that clear resets all properties.</summary>
[Fact]
public void Clear_ResetsAllProperties()
{
@@ -140,6 +151,7 @@ public class ReadWriteViewModelTests
_vm.WriteStatus.ShouldBeNull();
}
/// <summary>Verifies that IsNodeSelected tracks the selected node ID.</summary>
[Fact]
public void IsNodeSelected_TracksSelectedNodeId()
{
@@ -14,6 +14,7 @@ public class SubscriptionsViewModelTests
private readonly FakeOpcUaClientService _service;
private readonly SubscriptionsViewModel _vm;
/// <summary>Initializes test instance with fake services.</summary>
public SubscriptionsViewModelTests()
{
_service = new FakeOpcUaClientService();
@@ -21,6 +22,7 @@ public class SubscriptionsViewModelTests
_vm = new SubscriptionsViewModel(_service, dispatcher);
}
/// <summary>Verifies that AddSubscriptionCommand cannot execute when disconnected.</summary>
[Fact]
public void AddSubscriptionCommand_CannotExecute_WhenDisconnected()
{
@@ -29,6 +31,7 @@ public class SubscriptionsViewModelTests
_vm.AddSubscriptionCommand.CanExecute(null).ShouldBeFalse();
}
/// <summary>Verifies that AddSubscriptionCommand cannot execute without a node ID.</summary>
[Fact]
public void AddSubscriptionCommand_CannotExecute_WhenNoNodeId()
{
@@ -37,6 +40,7 @@ public class SubscriptionsViewModelTests
_vm.AddSubscriptionCommand.CanExecute(null).ShouldBeFalse();
}
/// <summary>Verifies that AddSubscriptionCommand adds a new subscription to the active list.</summary>
[Fact]
public async Task AddSubscriptionCommand_AddsItem()
{
@@ -53,6 +57,7 @@ public class SubscriptionsViewModelTests
_service.SubscribeCallCount.ShouldBe(1);
}
/// <summary>Verifies that RemoveSubscriptionCommand removes selected subscription.</summary>
[Fact]
public async Task RemoveSubscriptionCommand_RemovesItem()
{
@@ -68,6 +73,7 @@ public class SubscriptionsViewModelTests
_service.UnsubscribeCallCount.ShouldBe(1);
}
/// <summary>Verifies that RemoveSubscriptionCommand cannot execute without selection.</summary>
[Fact]
public void RemoveSubscriptionCommand_CannotExecute_WhenNoSelection()
{
@@ -76,6 +82,7 @@ public class SubscriptionsViewModelTests
_vm.RemoveSubscriptionCommand.CanExecute(null).ShouldBeFalse();
}
/// <summary>Verifies that DataChanged event updates the matching subscription row.</summary>
[Fact]
public async Task DataChanged_UpdatesMatchingRow()
{
@@ -90,6 +97,7 @@ public class SubscriptionsViewModelTests
_vm.ActiveSubscriptions[0].Status.ShouldNotBeNull();
}
/// <summary>Verifies that DataChanged event does not update non-matching subscription rows.</summary>
[Fact]
public async Task DataChanged_DoesNotUpdateNonMatchingRow()
{
@@ -103,6 +111,7 @@ public class SubscriptionsViewModelTests
_vm.ActiveSubscriptions[0].Value.ShouldBeNull();
}
/// <summary>Verifies that Clear removes all subscriptions.</summary>
[Fact]
public void Clear_RemovesAllSubscriptions()
{
@@ -115,6 +124,7 @@ public class SubscriptionsViewModelTests
_vm.SubscriptionCount.ShouldBe(0);
}
/// <summary>Verifies that Teardown unregisters the event handler.</summary>
[Fact]
public void Teardown_UnhooksEventHandler()
{
@@ -128,12 +138,14 @@ public class SubscriptionsViewModelTests
_vm.ActiveSubscriptions[0].Value.ShouldBeNull();
}
/// <summary>Verifies that default interval is 1000 milliseconds.</summary>
[Fact]
public void DefaultInterval_Is1000()
{
_vm.NewInterval.ShouldBe(1000);
}
/// <summary>Verifies that AddSubscriptionForNodeAsync adds a subscription.</summary>
[Fact]
public async Task AddSubscriptionForNodeAsync_AddsSubscription()
{
@@ -147,6 +159,7 @@ public class SubscriptionsViewModelTests
_service.SubscribeCallCount.ShouldBe(1);
}
/// <summary>Verifies that AddSubscriptionForNodeAsync skips duplicate subscriptions.</summary>
[Fact]
public async Task AddSubscriptionForNodeAsync_SkipsDuplicate()
{
@@ -159,6 +172,7 @@ public class SubscriptionsViewModelTests
_service.SubscribeCallCount.ShouldBe(1);
}
/// <summary>Verifies that AddSubscriptionForNodeAsync does nothing when disconnected.</summary>
[Fact]
public async Task AddSubscriptionForNodeAsync_DoesNothing_WhenDisconnected()
{
@@ -170,6 +184,7 @@ public class SubscriptionsViewModelTests
_service.SubscribeCallCount.ShouldBe(0);
}
/// <summary>Verifies that GetSubscribedNodeIds returns all active subscription node IDs.</summary>
[Fact]
public async Task GetSubscribedNodeIds_ReturnsActiveNodeIds()
{
@@ -184,6 +199,7 @@ public class SubscriptionsViewModelTests
ids.ShouldContain("ns=2;s=Node2");
}
/// <summary>Verifies that RestoreSubscriptionsAsync subscribes to all provided node IDs.</summary>
[Fact]
public async Task RestoreSubscriptionsAsync_SubscribesAllNodes()
{
@@ -195,6 +211,7 @@ public class SubscriptionsViewModelTests
_service.SubscribeCallCount.ShouldBe(2);
}
/// <summary>Verifies that ValidateAndWriteAsync returns true on successful write.</summary>
[Fact]
public async Task ValidateAndWriteAsync_SuccessReturnsTrue()
{
@@ -209,6 +226,7 @@ public class SubscriptionsViewModelTests
_service.WriteCallCount.ShouldBe(1);
}
/// <summary>Verifies that ValidateAndWriteAsync returns false when value parsing fails.</summary>
[Fact]
public async Task ValidateAndWriteAsync_ParseFailureReturnsFalse()
{
@@ -223,6 +241,7 @@ public class SubscriptionsViewModelTests
_service.WriteCallCount.ShouldBe(0);
}
/// <summary>Verifies that ValidateAndWriteAsync returns false when write fails.</summary>
[Fact]
public async Task ValidateAndWriteAsync_WriteFailureReturnsFalse()
{
@@ -236,6 +255,7 @@ public class SubscriptionsViewModelTests
message.ShouldContain("Access denied");
}
/// <summary>Verifies that ValidateAndWriteAsync returns false when status is bad.</summary>
[Fact]
public async Task ValidateAndWriteAsync_BadStatusReturnsFalse()
{
@@ -249,6 +269,7 @@ public class SubscriptionsViewModelTests
message.ShouldContain("Write failed");
}
/// <summary>Verifies that AddSubscriptionRecursiveAsync subscribes a variable directly.</summary>
[Fact]
public async Task AddSubscriptionRecursiveAsync_SubscribesVariableDirectly()
{
@@ -260,6 +281,7 @@ public class SubscriptionsViewModelTests
_vm.ActiveSubscriptions[0].NodeId.ShouldBe("ns=2;s=Var1");
}
/// <summary>Verifies that AddSubscriptionRecursiveAsync browses objects and subscribes variable children.</summary>
[Fact]
public async Task AddSubscriptionRecursiveAsync_BrowsesObjectAndSubscribesVariableChildren()
{
@@ -311,6 +333,7 @@ public class SubscriptionsViewModelTests
_vm.StatusMessage.ShouldContain("Bad node id");
}
/// <summary>Verifies that AddSubscriptionRecursiveAsync recurses through nested objects.</summary>
[Fact]
public async Task AddSubscriptionRecursiveAsync_RecursesNestedObjects()
{