docs: complete XML doc comments via fixdocs (2757 to 131 findings)

Add missing <returns>/<param>/<summary>/<typeparam> tags and clean up
misused inheritdoc across 481 files so the documented API surface is
complete. Documentation-only (zero code lines changed). The 131 remaining
findings are inheritdoc-style warnings deliberately left to preserve
hand-written implementation rationale (plan-decision notes, race-condition
explanations).
This commit is contained in:
Joseph Doherty
2026-06-03 12:34:34 -04:00
parent c6d9b20d9f
commit bd6c0b4d3d
481 changed files with 2550 additions and 1668 deletions
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class AlarmsCommandTests
{
/// <summary>Verifies that Execute subscribes to alarms.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task Execute_SubscribesToAlarms()
{
@@ -33,6 +34,7 @@ public class AlarmsCommandTests
}
/// <summary>Verifies that Execute with node passes source node ID.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task Execute_WithNode_PassesSourceNodeId()
{
@@ -58,6 +60,7 @@ public class AlarmsCommandTests
}
/// <summary>Verifies that Execute with refresh requests condition refresh.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task Execute_WithRefresh_RequestsConditionRefresh()
{
@@ -83,6 +86,7 @@ public class AlarmsCommandTests
}
/// <summary>Verifies that refresh failure prints error.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task Execute_RefreshFailure_PrintsError()
{
@@ -110,6 +114,7 @@ public class AlarmsCommandTests
}
/// <summary>Verifies that Execute unsubscribes on cancellation.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task Execute_UnsubscribesOnCancellation()
{
@@ -132,6 +137,7 @@ public class AlarmsCommandTests
}
/// <summary>Verifies that Execute disconnects in finally block.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task Execute_DisconnectsInFinally()
{
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class BrowseCommandTests
{
/// <summary>Verifies that Execute prints browse results correctly.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_PrintsBrowseResults()
{
@@ -37,6 +38,7 @@ public class BrowseCommandTests
}
/// <summary>Verifies that Execute browses from the specified node ID.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_BrowsesFromSpecifiedNode()
{
@@ -60,6 +62,7 @@ public class BrowseCommandTests
}
/// <summary>Verifies that Execute browses from null node when not specified.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_DefaultBrowsesFromNull()
{
@@ -81,6 +84,7 @@ public class BrowseCommandTests
}
/// <summary>Verifies that Execute browses only a single level when not recursive.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_NonRecursive_BrowsesSingleLevel()
{
@@ -106,6 +110,7 @@ public class BrowseCommandTests
}
/// <summary>Verifies that Execute browses child nodes when recursive flag is set.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_Recursive_BrowsesChildren()
{
@@ -129,6 +134,7 @@ public class BrowseCommandTests
}
/// <summary>Verifies that Execute disconnects and disposes in the finally block.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_DisconnectsInFinally()
{
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class CommandBaseTests
{
/// <summary>Verifies that common options map to connection settings correctly.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CommonOptions_MapToConnectionSettings_Correctly()
{
@@ -39,6 +40,7 @@ public class CommandBaseTests
}
/// <summary>Verifies that encrypt option maps to SignAndEncrypt.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SecurityOption_Encrypt_MapsToSignAndEncrypt()
{
@@ -57,6 +59,7 @@ public class CommandBaseTests
}
/// <summary>Verifies that none option maps to None.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SecurityOption_None_MapsToNone()
{
@@ -75,6 +78,7 @@ public class CommandBaseTests
}
/// <summary>Verifies that no failover URLs results in null FailoverUrls.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task NoFailoverUrls_FailoverUrlsIsNull()
{
@@ -13,6 +13,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class CommandRangeValidationTests
{
/// <summary>Verifies that BrowseCommand rejects negative depth values with a command exception.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task BrowseCommand_NegativeDepth_ThrowsCommandException()
{
@@ -30,6 +31,7 @@ public class CommandRangeValidationTests
}
/// <summary>Verifies that BrowseCommand rejects zero depth values with a command exception.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task BrowseCommand_ZeroDepth_ThrowsCommandException()
{
@@ -47,6 +49,7 @@ public class CommandRangeValidationTests
}
/// <summary>Verifies that SubscribeCommand rejects zero interval values with a command exception.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task SubscribeCommand_ZeroInterval_ThrowsCommandException()
{
@@ -65,6 +68,7 @@ public class CommandRangeValidationTests
}
/// <summary>Verifies that SubscribeCommand rejects negative interval values with a command exception.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task SubscribeCommand_NegativeInterval_ThrowsCommandException()
{
@@ -82,6 +86,7 @@ public class CommandRangeValidationTests
}
/// <summary>Verifies that SubscribeCommand in recursive mode rejects zero max depth with a command exception.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task SubscribeCommand_RecursiveZeroMaxDepth_ThrowsCommandException()
{
@@ -102,6 +107,7 @@ public class CommandRangeValidationTests
}
/// <summary>Verifies that SubscribeCommand rejects negative duration values with a command exception.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task SubscribeCommand_NegativeDuration_ThrowsCommandException()
{
@@ -119,6 +125,7 @@ public class CommandRangeValidationTests
}
/// <summary>Verifies that AlarmsCommand rejects zero interval values with a command exception.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task AlarmsCommand_ZeroInterval_ThrowsCommandException()
{
@@ -136,6 +143,7 @@ public class CommandRangeValidationTests
}
/// <summary>Verifies that HistoryReadCommand rejects negative max values with a command exception.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task HistoryReadCommand_NegativeMax_ThrowsCommandException()
{
@@ -154,6 +162,7 @@ public class CommandRangeValidationTests
}
/// <summary>Verifies that HistoryReadCommand rejects zero interval values with a command exception.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task HistoryReadCommand_ZeroInterval_ThrowsCommandException()
{
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class ConnectCommandTests
{
/// <summary>Verifies that execute prints connection info.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task Execute_PrintsConnectionInfo()
{
@@ -40,6 +41,7 @@ public class ConnectCommandTests
}
/// <summary>Verifies that execute calls connect and disconnect.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task Execute_CallsConnectAndDisconnect()
{
@@ -59,6 +61,7 @@ public class ConnectCommandTests
}
/// <summary>Verifies that execute disconnects on error.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task Execute_DisconnectsOnError()
{
@@ -15,6 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class EventHandlerLifecycleTests
{
/// <summary>Verifies that SubscribeCommand detaches the DataChanged event handler after exit.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscribeCommand_AfterExit_DataChangedEventHasNoSubscribers()
{
@@ -37,6 +38,7 @@ public class EventHandlerLifecycleTests
}
/// <summary>Verifies that AlarmsCommand detaches the AlarmEvent handler after exit.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task AlarmsCommand_AfterExit_AlarmEventHasNoSubscribers()
{
@@ -126,13 +126,13 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
/// <inheritdoc />
public ConnectionInfo? CurrentConnectionInfo => ConnectCalled ? ConnectionInfoResult : null;
/// <inheritdoc />
/// <summary>Raised when a subscribed node value changes.</summary>
public event EventHandler<DataChangedEventArgs>? DataChanged;
/// <inheritdoc />
/// <summary>Raised when an alarm event is received from the server.</summary>
public event EventHandler<AlarmEventArgs>? AlarmEvent;
/// <inheritdoc />
/// <summary>Raised when the connection state changes.</summary>
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
/// <summary>True when at least one handler is attached to <see cref="DataChanged" />.</summary>
@@ -17,6 +17,7 @@ public sealed class FakeOpcUaClientServiceFactory : IOpcUaClientServiceFactory
}
/// <summary>Creates and returns the fake OPC UA client service.</summary>
/// <returns>The pre-configured <see cref="FakeOpcUaClientService"/> instance.</returns>
public IOpcUaClientService Create()
{
return _service;
@@ -10,6 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class HistoryReadCommandTests
{
/// <summary>Verifies RawRead execution prints values.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task Execute_RawRead_PrintsValues()
{
@@ -44,6 +45,7 @@ public class HistoryReadCommandTests
}
/// <summary>Verifies RawRead execution calls HistoryReadRaw.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task Execute_RawRead_CallsHistoryReadRaw()
{
@@ -65,6 +67,7 @@ public class HistoryReadCommandTests
}
/// <summary>Verifies AggregateRead execution calls HistoryReadAggregate.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task Execute_AggregateRead_CallsHistoryReadAggregate()
{
@@ -87,6 +90,7 @@ public class HistoryReadCommandTests
}
/// <summary>Verifies AggregateRead execution prints aggregate info.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task Execute_AggregateRead_PrintsAggregateInfo()
{
@@ -109,6 +113,7 @@ public class HistoryReadCommandTests
}
/// <summary>Verifies invalid aggregate throws CommandException.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task Execute_InvalidAggregate_ThrowsCommandException()
{
@@ -129,6 +134,7 @@ public class HistoryReadCommandTests
}
/// <summary>Verifies disconnect is called in finally block.</summary>
/// <returns>A task that represents the asynchronous test.</returns>
[Fact]
public async Task Execute_DisconnectsInFinally()
{
@@ -14,6 +14,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class InputValidationErrorsTests
{
/// <summary>Verifies that HistoryReadCommand with invalid start time throws CommandException.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task HistoryReadCommand_InvalidStartTime_ThrowsCommandException()
{
@@ -32,6 +33,7 @@ public class InputValidationErrorsTests
}
/// <summary>Verifies that HistoryReadCommand with invalid end time throws CommandException.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task HistoryReadCommand_InvalidEndTime_ThrowsCommandException()
{
@@ -50,6 +52,7 @@ public class InputValidationErrorsTests
}
/// <summary>Verifies that HistoryReadCommand with invalid aggregate throws CommandException.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task HistoryReadCommand_InvalidAggregate_ThrowsCommandException()
{
@@ -68,6 +71,7 @@ public class InputValidationErrorsTests
}
/// <summary>Verifies that ReadCommand with invalid node ID throws CommandException.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadCommand_InvalidNodeId_ThrowsCommandException()
{
@@ -85,6 +89,7 @@ public class InputValidationErrorsTests
}
/// <summary>Verifies that SubscribeCommand with invalid node ID throws CommandException.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscribeCommand_InvalidNodeId_ThrowsCommandException()
{
@@ -15,6 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class LoggerLifecycleTests
{
/// <summary>Verifies that ConfigureLogging disposes previous logger before reassigning.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task ConfigureLogging_DisposesPreviousLogger_BeforeReassigning()
{
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class ReadCommandTests
{
/// <summary>Verifies that execute prints the read value.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_PrintsReadValue()
{
@@ -41,6 +42,7 @@ public class ReadCommandTests
}
/// <summary>Verifies that execute calls read value with correct node ID.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_CallsReadValueWithCorrectNodeId()
{
@@ -60,6 +62,7 @@ public class ReadCommandTests
}
/// <summary>Verifies that execute disconnects in finally.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_DisconnectsInFinally()
{
@@ -79,6 +82,7 @@ public class ReadCommandTests
}
/// <summary>Verifies that execute disconnects even on read error.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_DisconnectsEvenOnReadError()
{
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class RedundancyCommandTests
{
/// <summary>Verifies that Execute prints redundancy information correctly.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_PrintsRedundancyInfo()
{
@@ -36,6 +37,7 @@ public class RedundancyCommandTests
}
/// <summary>Verifies that Execute omits the Server URIs section when none are present.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_NoServerUris_OmitsUriSection()
{
@@ -61,6 +63,7 @@ public class RedundancyCommandTests
}
/// <summary>Verifies that Execute calls GetRedundancyInfo on the service.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_CallsGetRedundancyInfo()
{
@@ -78,6 +81,7 @@ public class RedundancyCommandTests
}
/// <summary>Verifies that Execute disconnects and disposes in the finally block.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_DisconnectsInFinally()
{
@@ -14,6 +14,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class SubscribeCommandSummaryTests
{
/// <summary>Verifies that nodes with no updates are counted separately from suspects.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task Summary_NodeWithNoUpdate_IsCountedAsNeverNotAsNeverWentBad()
{
@@ -41,6 +42,7 @@ public class SubscribeCommandSummaryTests
}
/// <summary>Verifies that nodes with only good values are counted as never went bad.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task Summary_NodeReceivedOnlyGoodValues_IsCountedAsNeverWentBad()
{
@@ -73,6 +75,7 @@ public class SubscribeCommandSummaryTests
}
/// <summary>Verifies that nodes with bad values are counted as ever went bad.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task Summary_NodeReceivedBadValue_IsCountedAsEverWentBad()
{
@@ -102,6 +105,7 @@ public class SubscribeCommandSummaryTests
}
/// <summary>Verifies that subscription auto-exits when duration expires.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task Duration_ZeroOrPositive_AutoExits()
{
@@ -129,6 +133,7 @@ public class SubscribeCommandSummaryTests
}
/// <summary>Verifies that --quiet suppresses updates but prints summary.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task Quiet_SuppressesPerUpdateOutputButPrintsSummary()
{
@@ -160,6 +165,7 @@ public class SubscribeCommandSummaryTests
}
/// <summary>Verifies that summary is written to disk when summary file is specified.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task SummaryFile_WritesSummaryToDisk()
{
@@ -191,6 +197,7 @@ public class SubscribeCommandSummaryTests
}
/// <summary>Verifies that recursive flag browses subtree and subscribes every variable.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task Recursive_BrowsesSubtreeAndSubscribesEveryVariable()
{
@@ -230,6 +237,7 @@ public class SubscribeCommandSummaryTests
}
/// <summary>Verifies that subscription failures are handled gracefully.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task SubscribeFailure_PrintsFailedMessage_DoesNotCrash()
{
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class SubscribeCommandTests
{
/// <summary>Verifies that ExecuteAsync subscribes with the correct parameters.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_SubscribesWithCorrectParameters()
{
@@ -37,6 +38,7 @@ public class SubscribeCommandTests
}
/// <summary>Verifies that ExecuteAsync unsubscribes when cancellation is requested.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_UnsubscribesOnCancellation()
{
@@ -60,6 +62,7 @@ public class SubscribeCommandTests
}
/// <summary>Verifies that ExecuteAsync disconnects and disposes in a finally block.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_DisconnectsInFinally()
{
@@ -84,6 +87,7 @@ public class SubscribeCommandTests
}
/// <summary>Verifies that ExecuteAsync prints the correct subscription message.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Execute_PrintsSubscriptionMessage()
{
@@ -10,6 +10,7 @@ public static class TestConsoleHelper
/// <summary>
/// Creates a new <see cref="FakeInMemoryConsole" /> for testing.
/// </summary>
/// <returns>A new <see cref="FakeInMemoryConsole"/> instance.</returns>
public static FakeInMemoryConsole CreateConsole()
{
return new FakeInMemoryConsole();
@@ -9,6 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests;
public class WriteCommandTests
{
/// <summary>Verifies that write command executes successfully.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task Execute_WritesSuccessfully()
{
@@ -33,6 +34,7 @@ public class WriteCommandTests
}
/// <summary>Verifies that write command reports failure.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task Execute_ReportsFailure()
{
@@ -57,6 +59,7 @@ public class WriteCommandTests
}
/// <summary>Verifies that write command reads current value before writing.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task Execute_ReadsCurrentValueThenWrites()
{
@@ -83,6 +86,7 @@ public class WriteCommandTests
}
/// <summary>Verifies that write command disconnects in finally block.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task Execute_DisconnectsInFinally()
{
@@ -12,11 +12,7 @@ internal sealed class FakeEndpointDiscovery : IEndpointDiscovery
/// <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>
/// <inheritdoc />
public EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl,
MessageSecurityMode requestedMode)
{
@@ -44,6 +44,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that a valid connection request returns populated connection metadata and marks the client as connected.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectAsync_ValidSettings_ReturnsConnectionInfo()
{
@@ -58,6 +59,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that invalid connection settings fail validation before any OPC UA session is created.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectAsync_InvalidSettings_ThrowsBeforeCreatingSession()
{
@@ -71,6 +73,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that server and security details from the session are copied into the exposed connection info.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectAsync_PopulatesConnectionInfo()
{
@@ -96,6 +99,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that connection-state transitions are raised for the connecting and connected phases.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectAsync_RaisesConnectionStateChangedEvents()
{
@@ -114,6 +118,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that a failed session creation leaves the client in the disconnected state.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectAsync_SessionFactoryFails_TransitionsToDisconnected()
{
@@ -130,6 +135,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that username and password settings are passed through to the session-creation pipeline.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectAsync_WithUsername_PassesThroughToFactory()
{
@@ -148,6 +154,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that disconnect closes the active session and clears exposed connection state.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DisconnectAsync_WhenConnected_ClosesSession()
{
@@ -164,6 +171,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that disconnect is safe to call when no server session is active.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DisconnectAsync_WhenNotConnected_IsIdempotent()
{
@@ -174,6 +182,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that repeated disconnect calls do not throw after cleanup has already run.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DisconnectAsync_CalledTwice_IsIdempotent()
{
@@ -187,6 +196,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that a connected client can read the current value of a node through the session adapter.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadValueAsync_WhenConnected_ReturnsValue()
{
@@ -206,6 +216,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that reads are rejected when the client is not connected to a server.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadValueAsync_WhenDisconnected_Throws()
{
@@ -216,6 +227,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that session-level read failures are surfaced to callers instead of being swallowed.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadValueAsync_SessionThrows_PropagatesException()
{
@@ -232,6 +244,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that writes succeed through the session adapter when the client is connected.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WriteValueAsync_WhenConnected_WritesValue()
{
@@ -252,6 +265,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that string inputs are coerced to the node's current data type before writing.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WriteValueAsync_StringValue_CoercesToTargetType()
{
@@ -271,6 +285,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that non-string values are written directly without an extra type-inference read.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WriteValueAsync_NonStringValue_WritesDirectly()
{
@@ -287,6 +302,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that writes are rejected when the client is disconnected.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WriteValueAsync_WhenDisconnected_Throws()
{
@@ -298,6 +314,7 @@ public class OpcUaClientServiceTests : IDisposable
/// Verifies that writing a string to a node whose current read returns a bad status
/// surfaces a clear error instead of writing a mistyped string value (Client.Shared-008).
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WriteValueAsync_StringValueWithBadReadStatus_ThrowsInvalidOperationException()
{
@@ -318,6 +335,7 @@ public class OpcUaClientServiceTests : IDisposable
/// Verifies that writing a string to a node whose read returns bad status and null Value
/// surfaces a clear error for both the bad-status case (Client.Shared-008).
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WriteValueAsync_StringValueWithBadStatus_MessageMentionsNode()
{
@@ -339,6 +357,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that browse results are mapped into the client browse model used by CLI and UI consumers.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task BrowseAsync_WhenConnected_ReturnsMappedResults()
{
@@ -368,6 +387,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that a null browse root defaults to the OPC UA Objects folder.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task BrowseAsync_NullParent_UsesObjectsFolder()
{
@@ -386,6 +406,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that object nodes trigger child-detection checks so the client can mark expandable branches.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task BrowseAsync_ObjectNode_ChecksHasChildren()
{
@@ -414,6 +435,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that browse continuation points are followed so multi-page address-space branches are fully returned.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task BrowseAsync_WithContinuationPoint_FollowsIt()
{
@@ -452,6 +474,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that browse requests are rejected when the client is disconnected.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task BrowseAsync_WhenDisconnected_Throws()
{
@@ -463,6 +486,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that subscribing to a node creates a monitored item on a data-change subscription.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscribeAsync_CreatesSubscription()
{
@@ -479,6 +503,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that duplicate subscribe requests for the same node do not create duplicate monitored items.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscribeAsync_DuplicateNode_IsIdempotent()
{
@@ -495,6 +520,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that data-change notifications from the subscription are raised through the shared client event.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscribeAsync_RaisesDataChangedEvent()
{
@@ -520,6 +546,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that unsubscribing removes the corresponding monitored item from the active subscription.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task UnsubscribeAsync_RemovesMonitoredItem()
{
@@ -537,6 +564,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that unsubscribing an unknown node is treated as a safe no-op.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task UnsubscribeAsync_WhenNotSubscribed_DoesNotThrow()
{
@@ -551,6 +579,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that data subscriptions cannot be created while the client is disconnected.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscribeAsync_WhenDisconnected_Throws()
{
@@ -563,6 +592,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that alarm subscription requests create an event monitored item on the session.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscribeAlarmsAsync_CreatesEventSubscription()
{
@@ -579,6 +609,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that duplicate alarm-subscription requests do not create duplicate event subscriptions.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscribeAlarmsAsync_Duplicate_IsIdempotent()
{
@@ -595,6 +626,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that OPC UA event notifications are mapped into the shared client alarm event model.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscribeAlarmsAsync_RaisesAlarmEvent()
{
@@ -643,6 +675,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that removing alarm monitoring deletes the underlying event subscription.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task UnsubscribeAlarmsAsync_DeletesSubscription()
{
@@ -660,6 +693,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that removing alarms is safe even when no alarm subscription exists.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task UnsubscribeAlarmsAsync_WhenNoSubscription_DoesNotThrow()
{
@@ -673,6 +707,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that condition refresh requests are forwarded to the active alarm subscription.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RequestConditionRefreshAsync_CallsAdapter()
{
@@ -690,6 +725,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that condition refresh fails fast when no alarm subscription is active.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RequestConditionRefreshAsync_NoAlarmSubscription_Throws()
{
@@ -704,6 +740,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that alarm subscriptions cannot be created while disconnected.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscribeAlarmsAsync_WhenDisconnected_Throws()
{
@@ -716,6 +753,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that raw history reads return the session-provided values.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task HistoryReadRawAsync_ReturnsValues()
{
@@ -738,6 +776,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that raw history reads are rejected while disconnected.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task HistoryReadRawAsync_WhenDisconnected_Throws()
{
@@ -748,6 +787,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that raw-history failures from the session are propagated to callers.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task HistoryReadRawAsync_SessionThrows_PropagatesException()
{
@@ -762,6 +802,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that aggregate history reads return the processed values from the session adapter.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task HistoryReadAggregateAsync_ReturnsValues()
{
@@ -784,6 +825,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that aggregate history reads are rejected while disconnected.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task HistoryReadAggregateAsync_WhenDisconnected_Throws()
{
@@ -796,6 +838,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that aggregate-history failures from the session are propagated to callers.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task HistoryReadAggregateAsync_SessionThrows_PropagatesException()
{
@@ -814,6 +857,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that redundancy mode, service level, and server URIs are read from the standard OPC UA redundancy nodes.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task GetRedundancyInfoAsync_ReturnsInfo()
{
@@ -846,6 +890,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that missing optional redundancy arrays do not prevent a redundancy snapshot from being returned.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task GetRedundancyInfoAsync_MissingOptionalArrays_ReturnsGracefully()
{
@@ -876,6 +921,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that redundancy inspection is rejected while disconnected.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task GetRedundancyInfoAsync_WhenDisconnected_Throws()
{
@@ -887,6 +933,7 @@ public class OpcUaClientServiceTests : IDisposable
/// Verifies that RedundancySupport boxed as a different numeric type (e.g. short) is handled
/// without InvalidCastException — defensive Convert.ToInt32 coercion (Client.Shared-002).
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task GetRedundancyInfoAsync_RedundancySupportBoxedAsShort_DoesNotThrow()
{
@@ -915,6 +962,7 @@ public class OpcUaClientServiceTests : IDisposable
/// Verifies that a bad-status response for RedundancySupport/ServiceLevel falls back to defaults
/// rather than throwing (Client.Shared-002).
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task GetRedundancyInfoAsync_BadStatusOnRequiredReads_ReturnsDefaults()
{
@@ -944,6 +992,7 @@ public class OpcUaClientServiceTests : IDisposable
/// Verifies that an alarm event with fewer than 6 fields (but at least 1) is still raised
/// with available fields — the old hard &lt;6 early return silently dropped it (Client.Shared-001).
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task OnAlarmEvent_TruncatedFields_StillRaisesEvent()
{
@@ -978,6 +1027,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that a null or empty event field list is silently ignored (defensive guard).
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task OnAlarmEvent_EmptyFields_DoesNotRaiseEvent()
{
@@ -1003,6 +1053,7 @@ public class OpcUaClientServiceTests : IDisposable
/// Verifies that a successful acknowledge call returns <see cref="StatusCodes.Good"/>
/// and reaches the session adapter's CallMethodAsync (Client.Shared-009).
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task AcknowledgeAlarmAsync_OnSuccess_ReturnsGood()
{
@@ -1022,6 +1073,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <see cref="ServiceResultException"/>, so callers using
/// <c>if (StatusCode.IsBad(result))</c> actually see the failure.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task AcknowledgeAlarmAsync_OnServiceResultException_ReturnsBadStatusCode()
{
@@ -1044,6 +1096,7 @@ public class OpcUaClientServiceTests : IDisposable
/// source node, but left alone when the caller already passes the condition node —
/// matches the documented contract.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task AcknowledgeAlarmAsync_LeavesConditionSuffixAlone()
{
@@ -1066,6 +1119,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <c>InAlarm</c>/<c>Acked</c> from the condition node's Galaxy attributes. Verify
/// the alarm event is delivered with the values from the supplemental reads.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task OnAlarmEvent_MissingAckedActiveButHasConditionNode_FallbackReadsAndRaisesEvent()
{
@@ -1139,6 +1193,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that a keep-alive failure moves the client to a configured failover endpoint.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task KeepAliveFailure_TriggersFailover()
{
@@ -1170,6 +1225,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that connection metadata is refreshed to reflect the newly active failover endpoint.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task KeepAliveFailure_UpdatesConnectionInfo()
{
@@ -1196,6 +1252,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that the client falls back to disconnected when every failover endpoint is unreachable.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task KeepAliveFailure_AllEndpointsFail_TransitionsToDisconnected()
{
@@ -1217,6 +1274,7 @@ public class OpcUaClientServiceTests : IDisposable
/// failover loop is still in-flight must be ignored, so only one failover runs and only
/// one replacement session is created.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task KeepAliveFailure_ReentrantWhileFailoverInFlight_RunsFailoverOnce()
{
@@ -1255,6 +1313,7 @@ public class OpcUaClientServiceTests : IDisposable
/// Regression for Client.Shared-005: concurrent subscribe/unsubscribe calls mutating the
/// active-subscription bookkeeping must not corrupt the dictionary or throw.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscribeAndUnsubscribe_ConcurrentCalls_DoNotCorruptState()
{
@@ -1283,6 +1342,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that dispose releases the underlying session and clears exposed connection state.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Dispose_CleansUpResources()
{
@@ -1299,6 +1359,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that dispose is safe to call even when no connection was established.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public void Dispose_WhenNotConnected_DoesNotThrow()
{
@@ -1308,6 +1369,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that public operations reject use after the shared client has been disposed.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task OperationsAfterDispose_Throw()
{
@@ -1324,6 +1386,7 @@ public class OpcUaClientServiceTests : IDisposable
/// <summary>
/// Verifies that the factory creates a usable shared OPC UA client service instance.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public void OpcUaClientServiceFactory_CreatesService()
{
@@ -47,6 +47,7 @@ public class AlarmsViewModelTests
}
/// <summary>Verifies that SubscribeCommand sets IsSubscribed flag.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscribeCommand_SetsIsSubscribed()
{
@@ -68,6 +69,7 @@ public class AlarmsViewModelTests
}
/// <summary>Verifies that UnsubscribeCommand clears IsSubscribed flag.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task UnsubscribeCommand_ClearsIsSubscribed()
{
@@ -81,6 +83,7 @@ public class AlarmsViewModelTests
}
/// <summary>Verifies that RefreshCommand calls the service.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RefreshCommand_CallsService()
{
@@ -156,6 +159,7 @@ public class AlarmsViewModelTests
/// Regression test for Client.UI-006 — when SubscribeAlarmsAsync throws, the failure must be
/// surfaced to the operator via the view model's StatusMessage rather than silently swallowed.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Subscribe_OnFailure_SurfacesStatusMessage()
{
@@ -30,6 +30,7 @@ public class BrowseTreeViewModelTests
}
/// <summary>Verifies that LoadRootsAsync populates root nodes.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task LoadRootsAsync_PopulatesRootNodes()
{
@@ -41,6 +42,7 @@ public class BrowseTreeViewModelTests
}
/// <summary>Verifies that LoadRootsAsync browses with null parent.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task LoadRootsAsync_BrowsesWithNullParent()
{
@@ -61,6 +63,7 @@ public class BrowseTreeViewModelTests
}
/// <summary>Verifies that nodes with children have a placeholder.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task LoadRootsAsync_NodeWithChildren_HasPlaceholder()
{
@@ -73,6 +76,7 @@ public class BrowseTreeViewModelTests
}
/// <summary>Verifies that nodes without children have no placeholder.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task LoadRootsAsync_NodeWithoutChildren_HasNoPlaceholder()
{
@@ -84,6 +88,7 @@ public class BrowseTreeViewModelTests
}
/// <summary>Verifies that first tree node expand triggers child browse.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task TreeNode_FirstExpand_TriggersChildBrowse()
{
@@ -114,6 +119,7 @@ public class BrowseTreeViewModelTests
}
/// <summary>Verifies that second tree node expand does not browse again.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task TreeNode_SecondExpand_DoesNotBrowseAgain()
{
@@ -143,6 +149,7 @@ public class BrowseTreeViewModelTests
}
/// <summary>Verifies that IsLoading transitions during browse.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task TreeNode_IsLoading_TransitionsDuringBrowse()
{
@@ -136,13 +136,13 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
/// <inheritdoc />
public ConnectionInfo? CurrentConnectionInfo { get; set; }
/// <inheritdoc />
/// <summary>Raised when a subscribed node value changes.</summary>
public event EventHandler<DataChangedEventArgs>? DataChanged;
/// <inheritdoc />
/// <summary>Raised when an alarm condition event is received from the server.</summary>
public event EventHandler<AlarmEventArgs>? AlarmEvent;
/// <inheritdoc />
/// <summary>Raised when the OPC UA session connection state changes.</summary>
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
/// <inheritdoc />
@@ -13,15 +13,14 @@ public sealed class FakeSettingsService : ISettingsService
/// <summary>Gets the last settings that were saved.</summary>
public UserSettings? LastSaved { get; private set; }
/// <summary>Loads and returns the current settings.</summary>
/// <inheritdoc />
public UserSettings Load()
{
LoadCallCount++;
return Settings;
}
/// <summary>Saves the specified settings.</summary>
/// <param name="settings">The settings to save.</param>
/// <inheritdoc />
public void Save(UserSettings settings)
{
SaveCallCount++;
@@ -60,6 +60,7 @@ public class HistoryViewModelTests
}
/// <summary>Verifies that a raw history read populates results correctly.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadHistoryCommand_Raw_PopulatesResults()
{
@@ -77,6 +78,7 @@ public class HistoryViewModelTests
}
/// <summary>Verifies that an aggregate history read populates results correctly.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadHistoryCommand_Aggregate_PopulatesResults()
{
@@ -94,6 +96,7 @@ public class HistoryViewModelTests
}
/// <summary>Verifies that the read history command clears previous results before loading new ones.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadHistoryCommand_ClearsResultsBefore()
{
@@ -107,6 +110,7 @@ public class HistoryViewModelTests
}
/// <summary>Verifies that the loading state is false after the read history command completes.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadHistoryCommand_IsLoading_FalseAfterComplete()
{
@@ -158,6 +162,7 @@ public class HistoryViewModelTests
}
/// <summary>Verifies that read history command errors are displayed in the results.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadHistoryCommand_Error_ShowsErrorInResults()
{
@@ -76,6 +76,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that a successful connect command updates the shell into the connected state.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectCommand_TransitionsToConnected()
{
@@ -89,6 +90,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that the initial browse tree is loaded after a successful connect.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectCommand_LoadsRootNodes()
{
@@ -101,6 +103,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that redundancy details are fetched and exposed after connecting.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectCommand_FetchesRedundancyInfo()
{
@@ -114,6 +117,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that the session label shows the connected server and session identity.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectCommand_SetsSessionLabel()
{
@@ -126,6 +130,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that disconnect returns the shell to the disconnected state.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DisconnectCommand_TransitionsToDisconnected()
{
@@ -140,6 +145,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that disconnect clears session-specific UI state such as browse data and redundancy details.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Disconnect_ClearsStateAndChildren()
{
@@ -155,6 +161,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that connection-state events from the client update the shell status text and state.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectionStateChangedEvent_UpdatesState()
{
@@ -171,6 +178,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that selecting a tree node updates the dependent read/write and history panels.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SelectedTreeNode_PropagatesToChildViewModels()
{
@@ -186,6 +194,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that a successful connect propagates connected state into the child tabs.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectCommand_PropagatesIsConnectedToChildViewModels()
{
@@ -200,6 +209,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that disconnect propagates disconnected state into the child tabs.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DisconnectCommand_PropagatesIsConnectedFalseToChildViewModels()
{
@@ -215,6 +225,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that failed connection attempts restore the disconnected shell state and surface the error text.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectFailure_RevertsToDisconnected()
{
@@ -229,6 +240,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that connection-state transitions raise property-changed notifications for UI binding updates.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task PropertyChanged_FiredForConnectionState()
{
@@ -260,6 +272,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that failover endpoint text is parsed into connection settings on connect.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectCommand_MapsFailoverUrlsToSettings()
{
@@ -276,6 +289,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that empty failover text is normalized to no configured failover endpoints.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectCommand_MapsEmptyFailoverUrlsToNull()
{
@@ -289,6 +303,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that the configured session timeout is passed into the connection settings.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectCommand_MapsSessionTimeoutToSettings()
{
@@ -302,6 +317,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that the auto-accept certificate toggle is passed into the connection settings.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectCommand_MapsAutoAcceptCertificatesToSettings()
{
@@ -315,6 +331,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that a custom certificate store path is passed into the connection settings.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectCommand_MapsCertificateStorePathToSettings()
{
@@ -328,6 +345,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that subscribing selected nodes adds subscriptions and switches the shell to the subscriptions tab.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscribeSelectedNodesCommand_SubscribesAndSwitchesToTab()
{
@@ -348,6 +366,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that subscribing selected nodes is a no-op when nothing is selected.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SubscribeSelectedNodesCommand_DoesNothing_WhenNoSelection()
{
@@ -361,6 +380,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that the history command targets the selected node and switches the shell to the history tab.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ViewHistoryForSelectedNodeCommand_SetsNodeAndSwitchesToTab()
{
@@ -378,6 +398,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that history actions are enabled when a variable node is selected.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task UpdateHistoryEnabledForSelection_TrueForVariableNode()
{
@@ -395,6 +416,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that history actions stay disabled when an object node rather than a variable is selected.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task UpdateHistoryEnabledForSelection_FalseForObjectNode()
{
@@ -470,6 +492,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that successful connections persist the current connection settings.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectCommand_SavesSettingsOnSuccess()
{
@@ -487,6 +510,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that failed connection attempts do not overwrite saved settings.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectCommand_DoesNotSaveOnFailure()
{
@@ -500,6 +524,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that active subscriptions are persisted when the shell disconnects.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectCommand_SavesSubscribedNodes()
{
@@ -522,6 +547,7 @@ public class MainWindowViewModelTests
/// view model must leave RedundancyInfo null without crashing or hiding the diagnostic.
/// The Status text is expected to remain "Connected" (redundancy is optional).
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectCommand_RedundancyFailure_DoesNotBreakConnection()
{
@@ -538,6 +564,7 @@ public class MainWindowViewModelTests
/// <summary>
/// Verifies that saved subscriptions are restored after reconnecting the shell.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ConnectCommand_RestoresSavedSubscriptions()
{
@@ -51,6 +51,7 @@ public class ReadWriteViewModelTests
}
/// <summary>Verifies that the read command updates value and status.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadCommand_UpdatesValueAndStatus()
{
@@ -91,6 +92,7 @@ public class ReadWriteViewModelTests
}
/// <summary>Verifies that the write command updates write status.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task WriteCommand_UpdatesWriteStatus()
{
@@ -118,6 +120,7 @@ public class ReadWriteViewModelTests
}
/// <summary>Verifies that read command error sets error status.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ReadCommand_Error_SetsErrorStatus()
{
@@ -41,6 +41,7 @@ public class SubscriptionsViewModelTests
}
/// <summary>Verifies that AddSubscriptionCommand adds a new subscription to the active list.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task AddSubscriptionCommand_AddsItem()
{
@@ -58,6 +59,7 @@ public class SubscriptionsViewModelTests
}
/// <summary>Verifies that RemoveSubscriptionCommand removes selected subscription.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task RemoveSubscriptionCommand_RemovesItem()
{
@@ -83,6 +85,7 @@ public class SubscriptionsViewModelTests
}
/// <summary>Verifies that DataChanged event updates the matching subscription row.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task DataChanged_UpdatesMatchingRow()
{
@@ -98,6 +101,7 @@ public class SubscriptionsViewModelTests
}
/// <summary>Verifies that DataChanged event does not update non-matching subscription rows.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task DataChanged_DoesNotUpdateNonMatchingRow()
{
@@ -146,6 +150,7 @@ public class SubscriptionsViewModelTests
}
/// <summary>Verifies that AddSubscriptionForNodeAsync adds a subscription.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task AddSubscriptionForNodeAsync_AddsSubscription()
{
@@ -160,6 +165,7 @@ public class SubscriptionsViewModelTests
}
/// <summary>Verifies that AddSubscriptionForNodeAsync skips duplicate subscriptions.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task AddSubscriptionForNodeAsync_SkipsDuplicate()
{
@@ -173,6 +179,7 @@ public class SubscriptionsViewModelTests
}
/// <summary>Verifies that AddSubscriptionForNodeAsync does nothing when disconnected.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task AddSubscriptionForNodeAsync_DoesNothing_WhenDisconnected()
{
@@ -185,6 +192,7 @@ public class SubscriptionsViewModelTests
}
/// <summary>Verifies that GetSubscribedNodeIds returns all active subscription node IDs.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task GetSubscribedNodeIds_ReturnsActiveNodeIds()
{
@@ -200,6 +208,7 @@ public class SubscriptionsViewModelTests
}
/// <summary>Verifies that RestoreSubscriptionsAsync subscribes to all provided node IDs.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task RestoreSubscriptionsAsync_SubscribesAllNodes()
{
@@ -212,6 +221,7 @@ public class SubscriptionsViewModelTests
}
/// <summary>Verifies that ValidateAndWriteAsync returns true on successful write.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task ValidateAndWriteAsync_SuccessReturnsTrue()
{
@@ -227,6 +237,7 @@ public class SubscriptionsViewModelTests
}
/// <summary>Verifies that ValidateAndWriteAsync returns false when value parsing fails.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task ValidateAndWriteAsync_ParseFailureReturnsFalse()
{
@@ -242,6 +253,7 @@ public class SubscriptionsViewModelTests
}
/// <summary>Verifies that ValidateAndWriteAsync returns false when write fails.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task ValidateAndWriteAsync_WriteFailureReturnsFalse()
{
@@ -256,6 +268,7 @@ public class SubscriptionsViewModelTests
}
/// <summary>Verifies that ValidateAndWriteAsync returns false when status is bad.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task ValidateAndWriteAsync_BadStatusReturnsFalse()
{
@@ -270,6 +283,7 @@ public class SubscriptionsViewModelTests
}
/// <summary>Verifies that AddSubscriptionRecursiveAsync subscribes a variable directly.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task AddSubscriptionRecursiveAsync_SubscribesVariableDirectly()
{
@@ -282,6 +296,7 @@ public class SubscriptionsViewModelTests
}
/// <summary>Verifies that AddSubscriptionRecursiveAsync browses objects and subscribes variable children.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task AddSubscriptionRecursiveAsync_BrowsesObjectAndSubscribesVariableChildren()
{
@@ -302,6 +317,7 @@ public class SubscriptionsViewModelTests
/// Regression test for Client.UI-006 — when SubscribeAsync throws, the failure must be surfaced
/// to the operator via the view model's StatusMessage rather than silently swallowed.
/// </summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task AddSubscription_OnFailure_SurfacesStatusMessage()
{
@@ -320,6 +336,7 @@ public class SubscriptionsViewModelTests
/// Regression test for Client.UI-006 — silent swallow when adding a subscription for a node
/// (the context-menu helper) must also surface a status to the operator.
/// </summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task AddSubscriptionForNodeAsync_OnFailure_SurfacesStatusMessage()
{
@@ -334,6 +351,7 @@ public class SubscriptionsViewModelTests
}
/// <summary>Verifies that AddSubscriptionRecursiveAsync recurses through nested objects.</summary>
/// <returns>A task that represents the asynchronous test operation.</returns>
[Fact]
public async Task AddSubscriptionRecursiveAsync_RecursesNestedObjects()
{