Compare commits

...

1 Commits

Author SHA1 Message Date
Joseph Doherty a1156960b9 docs: add missing XML doc comments across gateway, worker, and .NET client
Resolves 1113 documentation-completeness gaps flagged by CommentChecker
(MissingReturns, MissingInheritDoc, InheritDocMisused, MissingDoc,
MissingParam, RedundantInheritDoc) so the API surface is fully documented
and the analyzer scan is clean. Doc comments only; no code changes.
2026-06-03 12:33:53 -04:00
189 changed files with 1190 additions and 840 deletions
@@ -44,6 +44,7 @@ internal sealed class CliArguments
/// <summary>Returns whether the named flag was present in the arguments.</summary> /// <summary>Returns whether the named flag was present in the arguments.</summary>
/// <param name="name">The flag name (without '--' prefix).</param> /// <param name="name">The flag name (without '--' prefix).</param>
/// <returns>True if the flag was present; otherwise false.</returns>
public bool HasFlag(string name) public bool HasFlag(string name)
{ {
return _flags.Contains(name); return _flags.Contains(name);
@@ -51,6 +52,7 @@ internal sealed class CliArguments
/// <summary>Returns the value for a named argument, or <c>null</c> if absent.</summary> /// <summary>Returns the value for a named argument, or <c>null</c> if absent.</summary>
/// <param name="name">The argument name (without '--' prefix).</param> /// <param name="name">The argument name (without '--' prefix).</param>
/// <returns>The argument value, or null if the argument was not provided.</returns>
public string? GetOptional(string name) public string? GetOptional(string name)
{ {
return _values.TryGetValue(name, out string? value) return _values.TryGetValue(name, out string? value)
@@ -60,6 +62,7 @@ internal sealed class CliArguments
/// <summary>Returns the value for a required named argument, or throws if absent.</summary> /// <summary>Returns the value for a required named argument, or throws if absent.</summary>
/// <param name="name">The argument name (without '--' prefix).</param> /// <param name="name">The argument name (without '--' prefix).</param>
/// <returns>The argument value.</returns>
public string GetRequired(string name) public string GetRequired(string name)
{ {
string? value = GetOptional(name); string? value = GetOptional(name);
@@ -74,6 +77,7 @@ internal sealed class CliArguments
/// <summary>Parses and returns an int32 argument, or the default value if absent.</summary> /// <summary>Parses and returns an int32 argument, or the default value if absent.</summary>
/// <param name="name">The argument name (without '--' prefix).</param> /// <param name="name">The argument name (without '--' prefix).</param>
/// <param name="defaultValue">The default value if the argument is absent; if <c>null</c>, the argument is required.</param> /// <param name="defaultValue">The default value if the argument is absent; if <c>null</c>, the argument is required.</param>
/// <returns>The parsed int32 value, or the default if absent.</returns>
public int GetInt32(string name, int? defaultValue = null) public int GetInt32(string name, int? defaultValue = null)
{ {
string? value = GetOptional(name); string? value = GetOptional(name);
@@ -93,6 +97,7 @@ internal sealed class CliArguments
/// <summary>Parses and returns a uint32 argument, or the default value if absent.</summary> /// <summary>Parses and returns a uint32 argument, or the default value if absent.</summary>
/// <param name="name">The argument name (without '--' prefix).</param> /// <param name="name">The argument name (without '--' prefix).</param>
/// <param name="defaultValue">The default value if the argument is absent.</param> /// <param name="defaultValue">The default value if the argument is absent.</param>
/// <returns>The parsed uint32 value, or the default if absent.</returns>
public uint GetUInt32(string name, uint defaultValue) public uint GetUInt32(string name, uint defaultValue)
{ {
string? value = GetOptional(name); string? value = GetOptional(name);
@@ -104,6 +109,7 @@ internal sealed class CliArguments
/// <summary>Parses and returns a uint64 argument, or the default value if absent.</summary> /// <summary>Parses and returns a uint64 argument, or the default value if absent.</summary>
/// <param name="name">The argument name (without '--' prefix).</param> /// <param name="name">The argument name (without '--' prefix).</param>
/// <param name="defaultValue">The default value if the argument is absent.</param> /// <param name="defaultValue">The default value if the argument is absent.</param>
/// <returns>The parsed uint64 value, or the default if absent.</returns>
public ulong GetUInt64(string name, ulong defaultValue) public ulong GetUInt64(string name, ulong defaultValue)
{ {
string? value = GetOptional(name); string? value = GetOptional(name);
@@ -115,6 +121,7 @@ internal sealed class CliArguments
/// <summary>Parses and returns a TimeSpan argument, or the default value if absent. Supports "ms", "s", and standard TimeSpan format.</summary> /// <summary>Parses and returns a TimeSpan argument, or the default value if absent. Supports "ms", "s", and standard TimeSpan format.</summary>
/// <param name="name">The argument name (without '--' prefix).</param> /// <param name="name">The argument name (without '--' prefix).</param>
/// <param name="defaultValue">The default value if the argument is absent.</param> /// <param name="defaultValue">The default value if the argument is absent.</param>
/// <returns>The parsed TimeSpan value, or the default if absent.</returns>
public TimeSpan GetDuration(string name, TimeSpan defaultValue) public TimeSpan GetDuration(string name, TimeSpan defaultValue)
{ {
string? value = GetOptional(name); string? value = GetOptional(name);
@@ -100,7 +100,8 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
return _galaxyClient.Value.WatchDeployEventsRawAsync(request, cancellationToken); return _galaxyClient.Value.WatchDeployEventsRawAsync(request, cancellationToken);
} }
/// <inheritdoc /> /// <summary>Disposes the galaxy client (if created) and the underlying gateway client.</summary>
/// <returns>A value task that completes when both clients are disposed.</returns>
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
if (_galaxyClient.IsValueCreated) if (_galaxyClient.IsValueCreated)
@@ -6,6 +6,7 @@ internal static class MxGatewayCliSecretRedactor
/// <summary>Replaces occurrences of the API key in the value with a redacted placeholder.</summary> /// <summary>Replaces occurrences of the API key in the value with a redacted placeholder.</summary>
/// <param name="value">The message text to redact.</param> /// <param name="value">The message text to redact.</param>
/// <param name="apiKey">The API key to remove; no redaction if null or empty.</param> /// <param name="apiKey">The API key to remove; no redaction if null or empty.</param>
/// <returns>The message text with any API key occurrence replaced by <c>[redacted]</c>.</returns>
public static string Redact(string value, string? apiKey) public static string Redact(string value, string? apiKey)
{ {
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(apiKey)) if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(apiKey))
@@ -22,6 +22,7 @@ public static class MxGatewayClientCli
/// <param name="args">Command-line arguments (command name followed by options).</param> /// <param name="args">Command-line arguments (command name followed by options).</param>
/// <param name="standardOutput">TextWriter for command output.</param> /// <param name="standardOutput">TextWriter for command output.</param>
/// <param name="standardError">TextWriter for error messages.</param> /// <param name="standardError">TextWriter for error messages.</param>
/// <returns>The process exit code (0 for success, 1 for error).</returns>
public static int Run( public static int Run(
string[] args, string[] args,
TextWriter standardOutput, TextWriter standardOutput,
@@ -38,6 +39,7 @@ public static class MxGatewayClientCli
/// <param name="standardError">TextWriter for error messages.</param> /// <param name="standardError">TextWriter for error messages.</param>
/// <param name="clientFactory">Optional factory to create the gateway client; defaults to MxGatewayClient.Create.</param> /// <param name="clientFactory">Optional factory to create the gateway client; defaults to MxGatewayClient.Create.</param>
/// <param name="standardInput">Optional TextReader for batch-mode stdin; defaults to <see cref="Console.In"/>.</param> /// <param name="standardInput">Optional TextReader for batch-mode stdin; defaults to <see cref="Console.In"/>.</param>
/// <returns>A task that resolves to the process exit code (0 for success, 1 for error).</returns>
public static Task<int> RunAsync( public static Task<int> RunAsync(
string[] args, string[] args,
TextWriter standardOutput, TextWriter standardOutput,
@@ -14,6 +14,7 @@ public sealed class BrowseChildrenSmokeTests
/// Verifies that BrowseChildren returns a non-zero cache sequence and /// Verifies that BrowseChildren returns a non-zero cache sequence and
/// a consistent children/child-has-children count from a live gateway. /// a consistent children/child-has-children count from a live gateway.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact(Skip = "Set MXGATEWAY_API_KEY and MXGATEWAY_ENDPOINT to enable.")] [Fact(Skip = "Set MXGATEWAY_API_KEY and MXGATEWAY_ENDPOINT to enable.")]
public async Task BrowseChildren_LiveGateway_ReturnsRootsWithCacheSequence() public async Task BrowseChildren_LiveGateway_ReturnsRootsWithCacheSequence()
{ {
@@ -8,14 +8,10 @@ namespace ZB.MOM.WW.MxGateway.Client.Tests;
/// </summary> /// </summary>
internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions options) : IGalaxyRepositoryClientTransport internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions options) : IGalaxyRepositoryClientTransport
{ {
/// <summary> /// <inheritdoc />
/// Gets the gateway client options.
/// </summary>
public MxGatewayClientOptions Options { get; } = options; public MxGatewayClientOptions Options { get; } = options;
/// <summary> /// <inheritdoc />
/// Gets the raw gRPC client; always null for the fake.
/// </summary>
public GalaxyRepository.GalaxyRepositoryClient? RawClient => null; public GalaxyRepository.GalaxyRepositoryClient? RawClient => null;
/// <summary> /// <summary>
@@ -66,11 +62,7 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
/// </summary> /// </summary>
public Queue<Exception> DiscoverHierarchyExceptions { get; } = new(); public Queue<Exception> DiscoverHierarchyExceptions { get; } = new();
/// <summary> /// <inheritdoc />
/// Records the request and either throws a queued exception or returns the configured reply.
/// </summary>
/// <param name="request">The TestConnectionRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public Task<TestConnectionReply> TestConnectionAsync( public Task<TestConnectionReply> TestConnectionAsync(
TestConnectionRequest request, TestConnectionRequest request,
CallOptions callOptions) CallOptions callOptions)
@@ -84,11 +76,7 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
return Task.FromResult(TestConnectionReply); return Task.FromResult(TestConnectionReply);
} }
/// <summary> /// <inheritdoc />
/// Records the request and either throws a queued exception or returns the configured reply.
/// </summary>
/// <param name="request">The GetLastDeployTimeRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public Task<GetLastDeployTimeReply> GetLastDeployTimeAsync( public Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
GetLastDeployTimeRequest request, GetLastDeployTimeRequest request,
CallOptions callOptions) CallOptions callOptions)
@@ -102,11 +90,7 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
return Task.FromResult(GetLastDeployTimeReply); return Task.FromResult(GetLastDeployTimeReply);
} }
/// <summary> /// <inheritdoc />
/// Records the request and either throws a queued exception or returns the configured reply.
/// </summary>
/// <param name="request">The DiscoverHierarchyRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public Task<DiscoverHierarchyReply> DiscoverHierarchyAsync( public Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
DiscoverHierarchyRequest request, DiscoverHierarchyRequest request,
CallOptions callOptions) CallOptions callOptions)
@@ -135,11 +119,7 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
/// <summary>Queue of exceptions to throw from BrowseChildren; dequeued in FIFO order.</summary> /// <summary>Queue of exceptions to throw from BrowseChildren; dequeued in FIFO order.</summary>
public Queue<Exception> BrowseChildrenExceptions { get; } = new(); public Queue<Exception> BrowseChildrenExceptions { get; } = new();
/// <summary> /// <inheritdoc />
/// Records the request and either throws a queued exception or returns the configured reply.
/// </summary>
/// <param name="request">The BrowseChildrenRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public Task<BrowseChildrenReply> BrowseChildrenAsync( public Task<BrowseChildrenReply> BrowseChildrenAsync(
BrowseChildrenRequest request, BrowseChildrenRequest request,
CallOptions callOptions) CallOptions callOptions)
@@ -177,11 +157,7 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
/// </summary> /// </summary>
public Func<CancellationToken, Task>? WatchDeployEventsBeforeYield { get; set; } public Func<CancellationToken, Task>? WatchDeployEventsBeforeYield { get; set; }
/// <summary> /// <inheritdoc />
/// Records the request and streams events, checking for queued exceptions and calling WatchDeployEventsBeforeYield before each event.
/// </summary>
/// <param name="request">The WatchDeployEventsRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync( public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
WatchDeployEventsRequest request, WatchDeployEventsRequest request,
CallOptions callOptions) CallOptions callOptions)
@@ -11,14 +11,10 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
private readonly Queue<MxCommandReply> _invokeReplies = new(); private readonly Queue<MxCommandReply> _invokeReplies = new();
private readonly List<MxEvent> _events = []; private readonly List<MxEvent> _events = [];
/// <summary> /// <inheritdoc />
/// Gets the gateway client options.
/// </summary>
public MxGatewayClientOptions Options { get; } = options; public MxGatewayClientOptions Options { get; } = options;
/// <summary> /// <inheritdoc />
/// Gets null, since this is a test fake without a real gRPC client.
/// </summary>
public MxAccessGateway.MxAccessGatewayClient? RawClient => null; public MxAccessGateway.MxAccessGatewayClient? RawClient => null;
/// <summary> /// <summary>
@@ -102,11 +98,7 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
/// </summary> /// </summary>
public Queue<Exception> InvokeExceptions { get; } = new(); public Queue<Exception> InvokeExceptions { get; } = new();
/// <summary> /// <inheritdoc />
/// Verifies that the OpenSessionAsync call is recorded and returns the configured reply.
/// </summary>
/// <param name="request">The OpenSessionRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public Task<OpenSessionReply> OpenSessionAsync( public Task<OpenSessionReply> OpenSessionAsync(
OpenSessionRequest request, OpenSessionRequest request,
CallOptions callOptions) CallOptions callOptions)
@@ -120,11 +112,7 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
return Task.FromResult(OpenSessionReply); return Task.FromResult(OpenSessionReply);
} }
/// <summary> /// <inheritdoc />
/// Verifies that the CloseSessionAsync call is recorded and returns the configured reply.
/// </summary>
/// <param name="request">The CloseSessionRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public Task<CloseSessionReply> CloseSessionAsync( public Task<CloseSessionReply> CloseSessionAsync(
CloseSessionRequest request, CloseSessionRequest request,
CallOptions callOptions) CallOptions callOptions)
@@ -138,11 +126,7 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
return Task.FromResult(CloseSessionReply); return Task.FromResult(CloseSessionReply);
} }
/// <summary> /// <inheritdoc />
/// Verifies that the InvokeAsync call is recorded and returns the next enqueued reply.
/// </summary>
/// <param name="request">The MxCommandRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public Task<MxCommandReply> InvokeAsync( public Task<MxCommandReply> InvokeAsync(
MxCommandRequest request, MxCommandRequest request,
CallOptions callOptions) CallOptions callOptions)
@@ -156,11 +140,7 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
return Task.FromResult(_invokeReplies.Dequeue()); return Task.FromResult(_invokeReplies.Dequeue());
} }
/// <summary> /// <inheritdoc />
/// Verifies that the StreamEventsAsync call is recorded and yields all enqueued events.
/// </summary>
/// <param name="request">The StreamEventsRequest to process.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public async IAsyncEnumerable<MxEvent> StreamEventsAsync( public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
StreamEventsRequest request, StreamEventsRequest request,
CallOptions callOptions) CallOptions callOptions)
@@ -193,11 +173,7 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
_events.Add(gatewayEvent); _events.Add(gatewayEvent);
} }
/// <summary> /// <inheritdoc />
/// Records the acknowledge call and returns the next enqueued reply (or default).
/// </summary>
/// <param name="request">The acknowledge alarm request.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public Task<AcknowledgeAlarmReply> AcknowledgeAlarmAsync( public Task<AcknowledgeAlarmReply> AcknowledgeAlarmAsync(
AcknowledgeAlarmRequest request, AcknowledgeAlarmRequest request,
CallOptions callOptions) CallOptions callOptions)
@@ -218,11 +194,7 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
}); });
} }
/// <summary> /// <inheritdoc />
/// Records the query call and yields each enqueued snapshot.
/// </summary>
/// <param name="request">The query active alarms request.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public async IAsyncEnumerable<ActiveAlarmSnapshot> QueryActiveAlarmsAsync( public async IAsyncEnumerable<ActiveAlarmSnapshot> QueryActiveAlarmsAsync(
QueryActiveAlarmsRequest request, QueryActiveAlarmsRequest request,
CallOptions callOptions) CallOptions callOptions)
@@ -251,11 +223,7 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
_activeAlarmSnapshots.Add(snapshot); _activeAlarmSnapshots.Add(snapshot);
} }
/// <summary> /// <inheritdoc />
/// Records the stream-alarms call and yields each enqueued feed message.
/// </summary>
/// <param name="request">The stream alarms request.</param>
/// <param name="callOptions">Call options specifying RPC behavior.</param>
public async IAsyncEnumerable<AlarmFeedMessage> StreamAlarmsAsync( public async IAsyncEnumerable<AlarmFeedMessage> StreamAlarmsAsync(
StreamAlarmsRequest request, StreamAlarmsRequest request,
CallOptions callOptions) CallOptions callOptions)
@@ -9,6 +9,7 @@ public sealed class GalaxyRepositoryClientTests
/// <summary> /// <summary>
/// Verifies that TestConnectionAsync attaches the API key in request metadata and returns the Ok flag. /// Verifies that TestConnectionAsync attaches the API key in request metadata and returns the Ok flag.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task TestConnectionAsync_AttachesApiKeyMetadataAndReturnsOkFlag() public async Task TestConnectionAsync_AttachesApiKeyMetadataAndReturnsOkFlag()
{ {
@@ -27,6 +28,7 @@ public sealed class GalaxyRepositoryClientTests
/// <summary> /// <summary>
/// Verifies that TestConnectionAsync returns false when the server reports NotOk. /// Verifies that TestConnectionAsync returns false when the server reports NotOk.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task TestConnectionAsync_ReturnsFalseWhenServerReportsNotOk() public async Task TestConnectionAsync_ReturnsFalseWhenServerReportsNotOk()
{ {
@@ -42,6 +44,7 @@ public sealed class GalaxyRepositoryClientTests
/// <summary> /// <summary>
/// Verifies that GetLastDeployTimeAsync returns null when the server reports not present. /// Verifies that GetLastDeployTimeAsync returns null when the server reports not present.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task GetLastDeployTimeAsync_ReturnsNullWhenNotPresent() public async Task GetLastDeployTimeAsync_ReturnsNullWhenNotPresent()
{ {
@@ -58,6 +61,7 @@ public sealed class GalaxyRepositoryClientTests
/// <summary> /// <summary>
/// Verifies that GetLastDeployTimeAsync returns the timestamp when the server reports it present. /// Verifies that GetLastDeployTimeAsync returns the timestamp when the server reports it present.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task GetLastDeployTimeAsync_ReturnsTimestampWhenPresent() public async Task GetLastDeployTimeAsync_ReturnsTimestampWhenPresent()
{ {
@@ -79,6 +83,7 @@ public sealed class GalaxyRepositoryClientTests
/// <summary> /// <summary>
/// Verifies that DiscoverHierarchyAsync returns the objects from the server reply. /// Verifies that DiscoverHierarchyAsync returns the objects from the server reply.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply() public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply()
{ {
@@ -141,6 +146,7 @@ public sealed class GalaxyRepositoryClientTests
/// <summary> /// <summary>
/// Verifies that DiscoverHierarchyAsync propagates cancellation tokens to the transport. /// Verifies that DiscoverHierarchyAsync propagates cancellation tokens to the transport.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task DiscoverHierarchyAsync_PropagatesCancellationToTransport() public async Task DiscoverHierarchyAsync_PropagatesCancellationToTransport()
{ {
@@ -161,6 +167,7 @@ public sealed class GalaxyRepositoryClientTests
/// <summary> /// <summary>
/// Verifies that TestConnectionAsync retries on transient gRPC failures. /// Verifies that TestConnectionAsync retries on transient gRPC failures.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task DiscoverHierarchyAsync_WithRepeatedPageToken_ThrowsProtocolError() public async Task DiscoverHierarchyAsync_WithRepeatedPageToken_ThrowsProtocolError()
{ {
@@ -184,6 +191,7 @@ public sealed class GalaxyRepositoryClientTests
/// <summary> /// <summary>
/// Verifies that DiscoverHierarchyAsync maps typed filter options correctly to the request. /// Verifies that DiscoverHierarchyAsync maps typed filter options correctly to the request.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task DiscoverHierarchyAsync_WithOptions_MapsTypedFilters() public async Task DiscoverHierarchyAsync_WithOptions_MapsTypedFilters()
{ {
@@ -218,6 +226,7 @@ public sealed class GalaxyRepositoryClientTests
/// <summary> /// <summary>
/// Verifies that TestConnectionAsync retries on transient gRPC failures. /// Verifies that TestConnectionAsync retries on transient gRPC failures.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure() public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure()
{ {
@@ -235,6 +244,7 @@ public sealed class GalaxyRepositoryClientTests
/// <summary> /// <summary>
/// Verifies that DiscoverHierarchyAsync retries on transient gRPC failures. /// Verifies that DiscoverHierarchyAsync retries on transient gRPC failures.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task DiscoverHierarchyAsync_RetriesOnTransientGrpcFailure() public async Task DiscoverHierarchyAsync_RetriesOnTransientGrpcFailure()
{ {
@@ -251,6 +261,7 @@ public sealed class GalaxyRepositoryClientTests
/// <summary> /// <summary>
/// Verifies that WatchDeployEventsAsync delivers the bootstrap event. /// Verifies that WatchDeployEventsAsync delivers the bootstrap event.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task WatchDeployEventsAsync_DeliversBootstrapEvent() public async Task WatchDeployEventsAsync_DeliversBootstrapEvent()
{ {
@@ -287,6 +298,7 @@ public sealed class GalaxyRepositoryClientTests
/// <summary> /// <summary>
/// Verifies that WatchDeployEventsAsync delivers multiple events in order. /// Verifies that WatchDeployEventsAsync delivers multiple events in order.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task WatchDeployEventsAsync_DeliversMultipleEventsInOrder() public async Task WatchDeployEventsAsync_DeliversMultipleEventsInOrder()
{ {
@@ -325,6 +337,7 @@ public sealed class GalaxyRepositoryClientTests
/// <summary> /// <summary>
/// Verifies that WatchDeployEventsAsync stops iteration cleanly when cancelled. /// Verifies that WatchDeployEventsAsync stops iteration cleanly when cancelled.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task WatchDeployEventsAsync_CancellationStopsIterationCleanly() public async Task WatchDeployEventsAsync_CancellationStopsIterationCleanly()
{ {
@@ -369,6 +382,7 @@ public sealed class GalaxyRepositoryClientTests
/// <summary> /// <summary>
/// Verifies that WatchDeployEventsAsync throws ObjectDisposedException after the client is disposed. /// Verifies that WatchDeployEventsAsync throws ObjectDisposedException after the client is disposed.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task WatchDeployEventsAsync_ThrowsAfterDisposal() public async Task WatchDeployEventsAsync_ThrowsAfterDisposal()
{ {
@@ -384,6 +398,7 @@ public sealed class GalaxyRepositoryClientTests
/// <summary> /// <summary>
/// Verifies that TestConnectionAsync throws ObjectDisposedException after the client is disposed. /// Verifies that TestConnectionAsync throws ObjectDisposedException after the client is disposed.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task TestConnectionAsync_ThrowsAfterDisposal() public async Task TestConnectionAsync_ThrowsAfterDisposal()
{ {
@@ -12,6 +12,7 @@ public sealed class LazyBrowseNodeTests
/// Verifies that calling BrowseAsync with no parent returns the root nodes /// Verifies that calling BrowseAsync with no parent returns the root nodes
/// from the first BrowseChildren reply and surfaces the per-child has-children hint. /// from the first BrowseChildren reply and surfaces the per-child has-children hint.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task Browse_NoParent_ReturnsRoots() public async Task Browse_NoParent_ReturnsRoots()
{ {
@@ -36,6 +37,7 @@ public sealed class LazyBrowseNodeTests
/// <summary> /// <summary>
/// Verifies that ExpandAsync populates Children and marks the node expanded after one RPC. /// Verifies that ExpandAsync populates Children and marks the node expanded after one RPC.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task Expand_PopulatesChildrenAndMarksExpanded() public async Task Expand_PopulatesChildrenAndMarksExpanded()
{ {
@@ -62,6 +64,7 @@ public sealed class LazyBrowseNodeTests
/// <summary> /// <summary>
/// Verifies that a second ExpandAsync call is a no-op and issues no additional RPC. /// Verifies that a second ExpandAsync call is a no-op and issues no additional RPC.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task Expand_CalledTwice_NoSecondRpc() public async Task Expand_CalledTwice_NoSecondRpc()
{ {
@@ -86,6 +89,7 @@ public sealed class LazyBrowseNodeTests
/// <summary> /// <summary>
/// Verifies that an RPC failure (NotFound) during expand is wrapped in MxGatewayException. /// Verifies that an RPC failure (NotFound) during expand is wrapped in MxGatewayException.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task Expand_UnknownParent_ThrowsMxGatewayException() public async Task Expand_UnknownParent_ThrowsMxGatewayException()
{ {
@@ -113,6 +117,7 @@ public sealed class LazyBrowseNodeTests
/// <summary> /// <summary>
/// Verifies that ExpandAsync drains multi-page sibling replies and forwards the page token. /// Verifies that ExpandAsync drains multi-page sibling replies and forwards the page token.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task Expand_MultiPageSiblings_GathersAllPages() public async Task Expand_MultiPageSiblings_GathersAllPages()
{ {
@@ -147,6 +152,7 @@ public sealed class LazyBrowseNodeTests
/// <summary> /// <summary>
/// Verifies that ten concurrent ExpandAsync calls issue exactly one RPC, not ten. /// Verifies that ten concurrent ExpandAsync calls issue exactly one RPC, not ten.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task Expand_CalledConcurrently_OnlyFiresOneRpc() public async Task Expand_CalledConcurrently_OnlyFiresOneRpc()
{ {
@@ -178,6 +184,7 @@ public sealed class LazyBrowseNodeTests
/// <summary> /// <summary>
/// Verifies that BrowseChildrenOptions filter fields are forwarded to the BrowseChildren request. /// Verifies that BrowseChildrenOptions filter fields are forwarded to the BrowseChildren request.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task Browse_WithFilter_ForwardsToRequest() public async Task Browse_WithFilter_ForwardsToRequest()
{ {
@@ -12,6 +12,7 @@ namespace ZB.MOM.WW.MxGateway.Client.Tests;
public sealed class MxGatewayClientAlarmsTests public sealed class MxGatewayClientAlarmsTests
{ {
/// <summary>AcknowledgeAlarmAsync records request and returns reply.</summary> /// <summary>AcknowledgeAlarmAsync records request and returns reply.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task AcknowledgeAlarmAsync_RecordsRequestShapeAndReturnsReply() public async Task AcknowledgeAlarmAsync_RecordsRequestShapeAndReturnsReply()
{ {
@@ -48,6 +49,7 @@ public sealed class MxGatewayClientAlarmsTests
} }
/// <summary>AcknowledgeAlarmAsync honors cancellation.</summary> /// <summary>AcknowledgeAlarmAsync honors cancellation.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task AcknowledgeAlarmAsync_HonorsCancellation() public async Task AcknowledgeAlarmAsync_HonorsCancellation()
{ {
@@ -72,6 +74,7 @@ public sealed class MxGatewayClientAlarmsTests
} }
/// <summary>AcknowledgeAlarmAsync maps unauthenticated RPC exception to typed exception.</summary> /// <summary>AcknowledgeAlarmAsync maps unauthenticated RPC exception to typed exception.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task AcknowledgeAlarmAsync_MapsUnauthenticated_RpcException_ToTypedException() public async Task AcknowledgeAlarmAsync_MapsUnauthenticated_RpcException_ToTypedException()
{ {
@@ -97,6 +100,7 @@ public sealed class MxGatewayClientAlarmsTests
} }
/// <summary>QueryActiveAlarmsAsync streams enqueued snapshots.</summary> /// <summary>QueryActiveAlarmsAsync streams enqueued snapshots.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task QueryActiveAlarmsAsync_StreamsEnqueuedSnapshots() public async Task QueryActiveAlarmsAsync_StreamsEnqueuedSnapshots()
{ {
@@ -122,6 +126,7 @@ public sealed class MxGatewayClientAlarmsTests
} }
/// <summary>QueryActiveAlarmsAsync passes filter prefix.</summary> /// <summary>QueryActiveAlarmsAsync passes filter prefix.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task QueryActiveAlarmsAsync_PassesFilterPrefix() public async Task QueryActiveAlarmsAsync_PassesFilterPrefix()
{ {
@@ -142,6 +147,7 @@ public sealed class MxGatewayClientAlarmsTests
} }
/// <summary>QueryActiveAlarmsAsync honors cancellation during enumeration.</summary> /// <summary>QueryActiveAlarmsAsync honors cancellation during enumeration.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task QueryActiveAlarmsAsync_HonorsCancellationDuringEnumeration() public async Task QueryActiveAlarmsAsync_HonorsCancellationDuringEnumeration()
{ {
@@ -24,6 +24,7 @@ public sealed class MxGatewayClientCliTests
} }
/// <summary>Verifies that the version command with --json flag prints JSON protocol versions.</summary> /// <summary>Verifies that the version command with --json flag prints JSON protocol versions.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task RunAsync_VersionJson_PrintsJsonProtocolVersions() public async Task RunAsync_VersionJson_PrintsJsonProtocolVersions()
{ {
@@ -38,6 +39,7 @@ public sealed class MxGatewayClientCliTests
} }
/// <summary>Verifies that the write command builds a write request and prints JSON reply.</summary> /// <summary>Verifies that the write command builds a write request and prints JSON reply.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task RunAsync_Write_BuildsWriteCommandAndPrintsJsonReply() public async Task RunAsync_Write_BuildsWriteCommandAndPrintsJsonReply()
{ {
@@ -83,6 +85,7 @@ public sealed class MxGatewayClientCliTests
} }
/// <summary>Verifies that error output redacts sensitive API key values.</summary> /// <summary>Verifies that error output redacts sensitive API key values.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task RunAsync_ErrorOutput_RedactsApiKey() public async Task RunAsync_ErrorOutput_RedactsApiKey()
{ {
@@ -107,6 +110,7 @@ public sealed class MxGatewayClientCliTests
} }
/// <summary>Verifies that stream-events with max-events limit stops output in non-JSON format.</summary> /// <summary>Verifies that stream-events with max-events limit stops output in non-JSON format.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task RunAsync_StreamEvents_WithMaxEventsStopsNonJsonOutput() public async Task RunAsync_StreamEvents_WithMaxEventsStopsNonJsonOutput()
{ {
@@ -149,6 +153,7 @@ public sealed class MxGatewayClientCliTests
/// <summary>Verifies that stream-alarms with --max-events stops output and distinguishes payload cases.</summary> /// <summary>Verifies that stream-alarms with --max-events stops output and distinguishes payload cases.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task RunAsync_StreamAlarms_WithMaxEventsStopsAndDistinguishesPayloadCases() public async Task RunAsync_StreamAlarms_WithMaxEventsStopsAndDistinguishesPayloadCases()
{ {
@@ -188,6 +193,7 @@ public sealed class MxGatewayClientCliTests
} }
/// <summary>Verifies that acknowledge-alarm builds a request and prints the JSON reply.</summary> /// <summary>Verifies that acknowledge-alarm builds a request and prints the JSON reply.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task RunAsync_AcknowledgeAlarm_BuildsRequestAndPrintsJsonReply() public async Task RunAsync_AcknowledgeAlarm_BuildsRequestAndPrintsJsonReply()
{ {
@@ -230,6 +236,7 @@ public sealed class MxGatewayClientCliTests
} }
/// <summary>Verifies that smoke command closes opened session when a command fails.</summary> /// <summary>Verifies that smoke command closes opened session when a command fails.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task RunAsync_Smoke_WhenCommandFails_ClosesOpenedSession() public async Task RunAsync_Smoke_WhenCommandFails_ClosesOpenedSession()
{ {
@@ -261,6 +268,7 @@ public sealed class MxGatewayClientCliTests
} }
/// <summary>Verifies that galaxy-test-connection command prints JSON reply.</summary> /// <summary>Verifies that galaxy-test-connection command prints JSON reply.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task RunAsync_GalaxyTestConnection_PrintsJsonReply() public async Task RunAsync_GalaxyTestConnection_PrintsJsonReply()
{ {
@@ -291,6 +299,7 @@ public sealed class MxGatewayClientCliTests
} }
/// <summary>Verifies that galaxy-discover command prints hierarchy summary.</summary> /// <summary>Verifies that galaxy-discover command prints hierarchy summary.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task RunAsync_GalaxyDiscover_PrintsHierarchySummary() public async Task RunAsync_GalaxyDiscover_PrintsHierarchySummary()
{ {
@@ -361,6 +370,7 @@ public sealed class MxGatewayClientCliTests
} }
/// <summary>Verifies that galaxy-watch command prints text output for deploy events.</summary> /// <summary>Verifies that galaxy-watch command prints text output for deploy events.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task RunAsync_GalaxyWatch_PrintsTextOutputForEvents() public async Task RunAsync_GalaxyWatch_PrintsTextOutputForEvents()
{ {
@@ -415,6 +425,7 @@ public sealed class MxGatewayClientCliTests
} }
/// <summary>Verifies that galaxy-watch with --json emits one JSON object per event.</summary> /// <summary>Verifies that galaxy-watch with --json emits one JSON object per event.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task RunAsync_GalaxyWatch_JsonEmitsOneObjectPerEvent() public async Task RunAsync_GalaxyWatch_JsonEmitsOneObjectPerEvent()
{ {
@@ -450,6 +461,7 @@ public sealed class MxGatewayClientCliTests
} }
/// <summary>Verifies that batch mode dispatches a single version command and emits the EOR sentinel.</summary> /// <summary>Verifies that batch mode dispatches a single version command and emits the EOR sentinel.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task RunAsync_Batch_DispatchesVersionAndWritesEndOfRecord() public async Task RunAsync_Batch_DispatchesVersionAndWritesEndOfRecord()
{ {
@@ -476,6 +488,7 @@ public sealed class MxGatewayClientCliTests
} }
/// <summary>Verifies that batch mode routes per-command errors to stdout as JSON between EOR markers.</summary> /// <summary>Verifies that batch mode routes per-command errors to stdout as JSON between EOR markers.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task RunAsync_Batch_WritesErrorsToStdoutAsJson() public async Task RunAsync_Batch_WritesErrorsToStdoutAsJson()
{ {
@@ -520,6 +533,7 @@ public sealed class MxGatewayClientCliTests
/// against exit code 0. /// against exit code 0.
/// </summary> /// </summary>
/// <param name="command">The alarm subcommand to validate (e.g. "stream-alarms", "acknowledge-alarm").</param> /// <param name="command">The alarm subcommand to validate (e.g. "stream-alarms", "acknowledge-alarm").</param>
/// <returns>A task that represents the asynchronous operation.</returns>
[Theory] [Theory]
[InlineData("stream-alarms")] [InlineData("stream-alarms")]
[InlineData("acknowledge-alarm")] [InlineData("acknowledge-alarm")]
@@ -574,6 +588,7 @@ public sealed class MxGatewayClientCliTests
/// against a zero server handle. The fix must fail loudly with a /// against a zero server handle. The fix must fail loudly with a
/// descriptive <see cref="MxGatewayException"/>. /// descriptive <see cref="MxGatewayException"/>.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task RunAsync_BenchReadBulk_WhenRegisterReplyMissingTypedPayload_FailsLoudly() public async Task RunAsync_BenchReadBulk_WhenRegisterReplyMissingTypedPayload_FailsLoudly()
{ {
@@ -624,6 +639,7 @@ public sealed class MxGatewayClientCliTests
/// kept spinning until <c>--duration-seconds</c> elapsed. After the fix /// kept spinning until <c>--duration-seconds</c> elapsed. After the fix
/// the bench must exit promptly when the supplied token cancels. /// the bench must exit promptly when the supplied token cancels.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task RunAsync_BenchReadBulk_WhenSteadyStateLoopReceivesCancellation_ExitsPromptly() public async Task RunAsync_BenchReadBulk_WhenSteadyStateLoopReceivesCancellation_ExitsPromptly()
{ {
@@ -718,6 +734,7 @@ public sealed class MxGatewayClientCliTests
/// to ~49.7 days. The fix must reject negatives with a clear error. /// to ~49.7 days. The fix must reject negatives with a clear error.
/// </summary> /// </summary>
/// <param name="command">The bulk-read subcommand to validate (e.g. "read-bulk", "bench-read-bulk").</param> /// <param name="command">The bulk-read subcommand to validate (e.g. "read-bulk", "bench-read-bulk").</param>
/// <returns>A task that represents the asynchronous operation.</returns>
[Theory] [Theory]
[InlineData("read-bulk")] [InlineData("read-bulk")]
[InlineData("bench-read-bulk")] [InlineData("bench-read-bulk")]
@@ -880,7 +897,8 @@ public sealed class MxGatewayClientCliTests
/// <summary>Optional per-call handler that overrides queue-based behaviour.</summary> /// <summary>Optional per-call handler that overrides queue-based behaviour.</summary>
public Func<MxCommandRequest, CancellationToken, Task<MxCommandReply>>? InvokeHandler { get; init; } public Func<MxCommandRequest, CancellationToken, Task<MxCommandReply>>? InvokeHandler { get; init; }
/// <inheritdoc /> /// <summary>Releases resources held by the fake CLI client.</summary>
/// <returns>A completed value task.</returns>
public ValueTask DisposeAsync() public ValueTask DisposeAsync()
{ {
return ValueTask.CompletedTask; return ValueTask.CompletedTask;
@@ -7,6 +7,7 @@ namespace ZB.MOM.WW.MxGateway.Client.Tests;
public sealed class MxGatewayClientSessionTests public sealed class MxGatewayClientSessionTests
{ {
/// <summary>Verifies that open session attaches API key metadata and cancellation token.</summary> /// <summary>Verifies that open session attaches API key metadata and cancellation token.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task OpenSessionRawAsync_AttachesApiKeyMetadataAndCancellation() public async Task OpenSessionRawAsync_AttachesApiKeyMetadataAndCancellation()
{ {
@@ -22,6 +23,7 @@ public sealed class MxGatewayClientSessionTests
} }
/// <summary>Verifies that open session returns a session with the raw open reply.</summary> /// <summary>Verifies that open session returns a session with the raw open reply.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task OpenSessionAsync_ReturnsSessionWithRawOpenReply() public async Task OpenSessionAsync_ReturnsSessionWithRawOpenReply()
{ {
@@ -37,6 +39,7 @@ public sealed class MxGatewayClientSessionTests
} }
/// <summary>Verifies that register builds a register command and returns server handle.</summary> /// <summary>Verifies that register builds a register command and returns server handle.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task RegisterAsync_BuildsRegisterCommandAndReturnsServerHandle() public async Task RegisterAsync_BuildsRegisterCommandAndReturnsServerHandle()
{ {
@@ -62,6 +65,7 @@ public sealed class MxGatewayClientSessionTests
} }
/// <summary>Verifies that add item 2 builds a command with the specified context.</summary> /// <summary>Verifies that add item 2 builds a command with the specified context.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task AddItem2Async_BuildsAddItem2CommandWithContext() public async Task AddItem2Async_BuildsAddItem2CommandWithContext()
{ {
@@ -87,6 +91,7 @@ public sealed class MxGatewayClientSessionTests
} }
/// <summary>Verifies that write raw builds a write command with the raw value.</summary> /// <summary>Verifies that write raw builds a write command with the raw value.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task WriteRawAsync_BuildsWriteCommandWithRawValue() public async Task WriteRawAsync_BuildsWriteCommandWithRawValue()
{ {
@@ -118,6 +123,7 @@ public sealed class MxGatewayClientSessionTests
} }
/// <summary>Verifies that write 2 raw builds a write 2 command with value and timestamp.</summary> /// <summary>Verifies that write 2 raw builds a write 2 command with value and timestamp.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task Write2RawAsync_BuildsWrite2CommandWithValueAndTimestamp() public async Task Write2RawAsync_BuildsWrite2CommandWithValueAndTimestamp()
{ {
@@ -146,6 +152,7 @@ public sealed class MxGatewayClientSessionTests
} }
/// <summary>Verifies that subscribe bulk builds one command and returns per-item results.</summary> /// <summary>Verifies that subscribe bulk builds one command and returns per-item results.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task SubscribeBulkAsync_BuildsOneBulkCommandAndReturnsPerItemResults() public async Task SubscribeBulkAsync_BuildsOneBulkCommandAndReturnsPerItemResults()
{ {
@@ -185,6 +192,7 @@ public sealed class MxGatewayClientSessionTests
} }
/// <summary>Verifies that stream events yields events in the order received from the gateway.</summary> /// <summary>Verifies that stream events yields events in the order received from the gateway.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task StreamEventsAsync_YieldsEventsInGatewayOrder() public async Task StreamEventsAsync_YieldsEventsInGatewayOrder()
{ {
@@ -216,6 +224,7 @@ public sealed class MxGatewayClientSessionTests
} }
/// <summary>Verifies that close is explicit and idempotent.</summary> /// <summary>Verifies that close is explicit and idempotent.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task CloseAsync_IsExplicitAndIdempotent() public async Task CloseAsync_IsExplicitAndIdempotent()
{ {
@@ -232,6 +241,7 @@ public sealed class MxGatewayClientSessionTests
} }
/// <summary>Verifies that invoke retries safe diagnostic commands on transient RPC failure.</summary> /// <summary>Verifies that invoke retries safe diagnostic commands on transient RPC failure.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task InvokeAsync_RetriesSafeDiagnosticCommandOnTransientGrpcFailure() public async Task InvokeAsync_RetriesSafeDiagnosticCommandOnTransientGrpcFailure()
{ {
@@ -256,6 +266,7 @@ public sealed class MxGatewayClientSessionTests
} }
/// <summary>Verifies that open session does not retry on transient RPC failure.</summary> /// <summary>Verifies that open session does not retry on transient RPC failure.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task OpenSessionAsync_DoesNotRetryTransientGrpcFailure() public async Task OpenSessionAsync_DoesNotRetryTransientGrpcFailure()
{ {
@@ -269,6 +280,7 @@ public sealed class MxGatewayClientSessionTests
} }
/// <summary>Verifies that invoke does not retry write commands on transient RPC failure.</summary> /// <summary>Verifies that invoke does not retry write commands on transient RPC failure.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task InvokeAsync_DoesNotRetryWriteCommand() public async Task InvokeAsync_DoesNotRetryWriteCommand()
{ {
@@ -284,6 +296,7 @@ public sealed class MxGatewayClientSessionTests
} }
/// <summary>Verifies that invoke helpers pass cancellation token to the transport.</summary> /// <summary>Verifies that invoke helpers pass cancellation token to the transport.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task InvokeHelpers_PassCancellationTokenToTransport() public async Task InvokeHelpers_PassCancellationTokenToTransport()
{ {
@@ -3,6 +3,7 @@ namespace ZB.MOM.WW.MxGateway.Client.Tests;
public sealed class MxGatewayGeneratedContractTests public sealed class MxGatewayGeneratedContractTests
{ {
/// <summary>Verifies that the generated gRPC client can be instantiated from the client factory.</summary> /// <summary>Verifies that the generated gRPC client can be instantiated from the client factory.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact] [Fact]
public async Task GeneratedGrpcClient_CanBeConstructedFromClientFactory() public async Task GeneratedGrpcClient_CanBeConstructedFromClientFactory()
{ {
@@ -337,6 +337,9 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
cancellationToken); cancellationToken);
} }
/// <summary>Builds a <see cref="BrowseChildrenRequest"/> from the provided options.</summary>
/// <param name="options">Browse children options to convert.</param>
/// <returns>The constructed request message.</returns>
internal static BrowseChildrenRequest BuildBrowseChildrenRequest(BrowseChildrenOptions options) internal static BrowseChildrenRequest BuildBrowseChildrenRequest(BrowseChildrenOptions options)
{ {
ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(options);
@@ -424,6 +427,7 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
/// <summary> /// <summary>
/// Closes the gRPC channel and releases resources. /// Closes the gRPC channel and releases resources.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous dispose operation.</returns>
public ValueTask DisposeAsync() public ValueTask DisposeAsync()
{ {
if (_disposed) if (_disposed)
@@ -493,6 +497,9 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
private static HttpMessageHandler CreateHttpHandler(MxGatewayClientOptions options) => private static HttpMessageHandler CreateHttpHandler(MxGatewayClientOptions options) =>
CreateHttpHandlerForTests(options); CreateHttpHandlerForTests(options);
/// <summary>Creates an <see cref="HttpMessageHandler"/> configured from the provided options for test use.</summary>
/// <param name="options">Client options used to configure TLS and timeouts.</param>
/// <returns>The configured HTTP message handler.</returns>
internal static SocketsHttpHandler CreateHttpHandlerForTests(MxGatewayClientOptions options) internal static SocketsHttpHandler CreateHttpHandlerForTests(MxGatewayClientOptions options)
{ {
SocketsHttpHandler handler = new() SocketsHttpHandler handler = new()
@@ -10,9 +10,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
MxGatewayClientOptions options, MxGatewayClientOptions options,
GalaxyRepository.GalaxyRepositoryClient rawClient) : IGalaxyRepositoryClientTransport GalaxyRepository.GalaxyRepositoryClient rawClient) : IGalaxyRepositoryClientTransport
{ {
/// <summary> /// <inheritdoc />
/// Gets the gateway client options.
/// </summary>
public MxGatewayClientOptions Options { get; } = options; public MxGatewayClientOptions Options { get; } = options;
/// <summary> /// <summary>
@@ -91,7 +89,11 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
} }
} }
/// <inheritdoc /> /// <summary>Streams deploy events from the Galaxy Repository, using an explicit cancellation token that overrides the call options token when provided.</summary>
/// <param name="request">The watch deploy events request.</param>
/// <param name="callOptions">Call options for the underlying gRPC call.</param>
/// <param name="cancellationToken">Optional cancellation token; takes precedence over the token in <paramref name="callOptions"/> when cancellable.</param>
/// <returns>An async enumerable of deploy events.</returns>
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync( public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
WatchDeployEventsRequest request, WatchDeployEventsRequest request,
CallOptions callOptions, CallOptions callOptions,
@@ -10,9 +10,7 @@ internal sealed class GrpcMxGatewayClientTransport(
MxGatewayClientOptions options, MxGatewayClientOptions options,
MxAccessGateway.MxAccessGatewayClient rawClient) : IMxGatewayClientTransport MxAccessGateway.MxAccessGatewayClient rawClient) : IMxGatewayClientTransport
{ {
/// <summary> /// <inheritdoc />
/// Gets the gateway client options.
/// </summary>
public MxGatewayClientOptions Options { get; } = options; public MxGatewayClientOptions Options { get; } = options;
/// <summary> /// <summary>
@@ -74,7 +72,11 @@ internal sealed class GrpcMxGatewayClientTransport(
} }
} }
/// <inheritdoc /> /// <summary>Streams MXAccess events from the gateway, forwarding an explicit cancellation token to the stream reader.</summary>
/// <param name="request">The stream events request.</param>
/// <param name="callOptions">gRPC call options.</param>
/// <param name="cancellationToken">Token to cancel the streaming enumeration.</param>
/// <returns>An async enumerable of MXAccess events.</returns>
public async IAsyncEnumerable<MxEvent> StreamEventsAsync( public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
StreamEventsRequest request, StreamEventsRequest request,
CallOptions callOptions, CallOptions callOptions,
@@ -133,7 +135,11 @@ internal sealed class GrpcMxGatewayClientTransport(
} }
} }
/// <inheritdoc /> /// <summary>Queries active alarms from the gateway, forwarding an explicit cancellation token to the stream reader.</summary>
/// <param name="request">The query active alarms request.</param>
/// <param name="callOptions">gRPC call options.</param>
/// <param name="cancellationToken">Token to cancel the streaming enumeration.</param>
/// <returns>An async enumerable of active alarm snapshots.</returns>
public async IAsyncEnumerable<ActiveAlarmSnapshot> QueryActiveAlarmsAsync( public async IAsyncEnumerable<ActiveAlarmSnapshot> QueryActiveAlarmsAsync(
QueryActiveAlarmsRequest request, QueryActiveAlarmsRequest request,
CallOptions callOptions, CallOptions callOptions,
@@ -175,7 +181,11 @@ internal sealed class GrpcMxGatewayClientTransport(
return QueryActiveAlarmsAsync(request, callOptions); return QueryActiveAlarmsAsync(request, callOptions);
} }
/// <inheritdoc /> /// <summary>Streams alarm feed messages from the gateway, forwarding an explicit cancellation token to the stream reader.</summary>
/// <param name="request">The stream alarms request.</param>
/// <param name="callOptions">gRPC call options.</param>
/// <param name="cancellationToken">Token to cancel the streaming enumeration.</param>
/// <returns>An async enumerable of alarm feed messages.</returns>
public async IAsyncEnumerable<AlarmFeedMessage> StreamAlarmsAsync( public async IAsyncEnumerable<AlarmFeedMessage> StreamAlarmsAsync(
StreamAlarmsRequest request, StreamAlarmsRequest request,
CallOptions callOptions, CallOptions callOptions,
@@ -15,6 +15,7 @@ internal interface IGalaxyRepositoryClientTransport
/// <summary>Tests the connection to the Galaxy Repository server.</summary> /// <summary>Tests the connection to the Galaxy Repository server.</summary>
/// <param name="request">The test connection request.</param> /// <param name="request">The test connection request.</param>
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param> /// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
/// <returns>A task that resolves to the test connection reply.</returns>
Task<TestConnectionReply> TestConnectionAsync( Task<TestConnectionReply> TestConnectionAsync(
TestConnectionRequest request, TestConnectionRequest request,
CallOptions callOptions); CallOptions callOptions);
@@ -22,6 +23,7 @@ internal interface IGalaxyRepositoryClientTransport
/// <summary>Gets the last deploy time from the Galaxy Repository server.</summary> /// <summary>Gets the last deploy time from the Galaxy Repository server.</summary>
/// <param name="request">The get last deploy time request.</param> /// <param name="request">The get last deploy time request.</param>
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param> /// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
/// <returns>A task that resolves to the last deploy time reply.</returns>
Task<GetLastDeployTimeReply> GetLastDeployTimeAsync( Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
GetLastDeployTimeRequest request, GetLastDeployTimeRequest request,
CallOptions callOptions); CallOptions callOptions);
@@ -29,6 +31,7 @@ internal interface IGalaxyRepositoryClientTransport
/// <summary>Discovers the object hierarchy in the Galaxy Repository.</summary> /// <summary>Discovers the object hierarchy in the Galaxy Repository.</summary>
/// <param name="request">The discover hierarchy request.</param> /// <param name="request">The discover hierarchy request.</param>
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param> /// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
/// <returns>A task that resolves to the hierarchy discovery reply.</returns>
Task<DiscoverHierarchyReply> DiscoverHierarchyAsync( Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
DiscoverHierarchyRequest request, DiscoverHierarchyRequest request,
CallOptions callOptions); CallOptions callOptions);
@@ -36,6 +39,7 @@ internal interface IGalaxyRepositoryClientTransport
/// <summary>Returns direct children of a parent in the Galaxy hierarchy.</summary> /// <summary>Returns direct children of a parent in the Galaxy hierarchy.</summary>
/// <param name="request">The browse children request.</param> /// <param name="request">The browse children request.</param>
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param> /// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
/// <returns>A task that resolves to the browse children reply.</returns>
Task<BrowseChildrenReply> BrowseChildrenAsync( Task<BrowseChildrenReply> BrowseChildrenAsync(
BrowseChildrenRequest request, BrowseChildrenRequest request,
CallOptions callOptions); CallOptions callOptions);
@@ -43,6 +47,7 @@ internal interface IGalaxyRepositoryClientTransport
/// <summary>Watches for deployment events from the Galaxy Repository server.</summary> /// <summary>Watches for deployment events from the Galaxy Repository server.</summary>
/// <param name="request">The watch deploy events request.</param> /// <param name="request">The watch deploy events request.</param>
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param> /// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
/// <returns>An async enumerable of deploy events.</returns>
IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync( IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
WatchDeployEventsRequest request, WatchDeployEventsRequest request,
CallOptions callOptions); CallOptions callOptions);
@@ -16,6 +16,11 @@ public sealed class LazyBrowseNode
private readonly SemaphoreSlim _expandLock = new(1, 1); private readonly SemaphoreSlim _expandLock = new(1, 1);
private bool _isExpanded; private bool _isExpanded;
/// <summary>Initializes a new instance of <see cref="LazyBrowseNode"/>.</summary>
/// <param name="client">The repository client used to fetch children.</param>
/// <param name="object">The underlying Galaxy object for this node.</param>
/// <param name="hasChildrenHint">True when the server reports the node has at least one matching descendant.</param>
/// <param name="options">Options controlling child browse behavior.</param>
internal LazyBrowseNode( internal LazyBrowseNode(
GalaxyRepositoryClient client, GalaxyRepositoryClient client,
GalaxyObject @object, GalaxyObject @object,
@@ -49,6 +54,7 @@ public sealed class LazyBrowseNode
/// (after the first completes) return immediately. /// (after the first completes) return immediately.
/// </remarks> /// </remarks>
/// <param name="cancellationToken">Token to observe for cancellation.</param> /// <param name="cancellationToken">Token to observe for cancellation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task ExpandAsync(CancellationToken cancellationToken = default) public async Task ExpandAsync(CancellationToken cancellationToken = default)
{ {
if (_isExpanded) if (_isExpanded)
@@ -7,6 +7,7 @@ public static class MxCommandReplyExtensions
{ {
/// <summary>Validates that the reply has a successful protocol status (Ok or MxAccessFailure), throwing a gateway exception if not.</summary> /// <summary>Validates that the reply has a successful protocol status (Ok or MxAccessFailure), throwing a gateway exception if not.</summary>
/// <param name="reply">The command reply to check.</param> /// <param name="reply">The command reply to check.</param>
/// <returns>The same <paramref name="reply"/> for fluent chaining when validation passes.</returns>
public static MxCommandReply EnsureProtocolSuccess(this MxCommandReply reply) public static MxCommandReply EnsureProtocolSuccess(this MxCommandReply reply)
{ {
ArgumentNullException.ThrowIfNull(reply); ArgumentNullException.ThrowIfNull(reply);
@@ -24,6 +25,7 @@ public static class MxCommandReplyExtensions
/// <summary>Validates that the reply indicates MXAccess success (no HResult or status failures), throwing MxAccessException if not.</summary> /// <summary>Validates that the reply indicates MXAccess success (no HResult or status failures), throwing MxAccessException if not.</summary>
/// <param name="reply">The command reply to check.</param> /// <param name="reply">The command reply to check.</param>
/// <returns>The same <paramref name="reply"/> for fluent chaining when validation passes.</returns>
public static MxCommandReply EnsureMxAccessSuccess(this MxCommandReply reply) public static MxCommandReply EnsureMxAccessSuccess(this MxCommandReply reply)
{ {
ArgumentNullException.ThrowIfNull(reply); ArgumentNullException.ThrowIfNull(reply);
@@ -249,6 +249,7 @@ public sealed class MxGatewayClient : IAsyncDisposable
/// <summary> /// <summary>
/// Disposes the client and releases all resources. /// Disposes the client and releases all resources.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous dispose operation.</returns>
public ValueTask DisposeAsync() public ValueTask DisposeAsync()
{ {
if (_disposed) if (_disposed)
@@ -318,6 +319,9 @@ public sealed class MxGatewayClient : IAsyncDisposable
private static HttpMessageHandler CreateHttpHandler(MxGatewayClientOptions options) => private static HttpMessageHandler CreateHttpHandler(MxGatewayClientOptions options) =>
CreateHttpHandlerForTests(options); CreateHttpHandlerForTests(options);
/// <summary>Creates an <see cref="HttpMessageHandler"/> configured from the provided options for test use.</summary>
/// <param name="options">Client options used to configure TLS and timeouts.</param>
/// <returns>The configured HTTP message handler.</returns>
internal static SocketsHttpHandler CreateHttpHandlerForTests(MxGatewayClientOptions options) internal static SocketsHttpHandler CreateHttpHandlerForTests(MxGatewayClientOptions options)
{ {
SocketsHttpHandler handler = new() SocketsHttpHandler handler = new()
@@ -12,6 +12,7 @@ internal static class MxGatewayClientRetryPolicy
/// <summary>Creates a Polly ResiliencePipeline that retries transient gRPC failures with exponential backoff.</summary> /// <summary>Creates a Polly ResiliencePipeline that retries transient gRPC failures with exponential backoff.</summary>
/// <param name="options">Retry configuration (max attempts, delay bounds, jitter).</param> /// <param name="options">Retry configuration (max attempts, delay bounds, jitter).</param>
/// <param name="logger">Optional logger for retry diagnostics.</param> /// <param name="logger">Optional logger for retry diagnostics.</param>
/// <returns>A configured <see cref="ResiliencePipeline"/> with exponential-backoff retry.</returns>
public static ResiliencePipeline Create( public static ResiliencePipeline Create(
MxGatewayClientRetryOptions options, MxGatewayClientRetryOptions options,
ILogger? logger) ILogger? logger)
@@ -42,6 +43,7 @@ internal static class MxGatewayClientRetryPolicy
/// <summary>Returns whether a command kind is eligible for automatic retry on transient failures.</summary> /// <summary>Returns whether a command kind is eligible for automatic retry on transient failures.</summary>
/// <param name="kind">The command kind to check.</param> /// <param name="kind">The command kind to check.</param>
/// <returns><see langword="true"/> if the command kind is safe to retry; otherwise <see langword="false"/>.</returns>
public static bool IsRetryableCommand(MxCommandKind kind) public static bool IsRetryableCommand(MxCommandKind kind)
{ {
return kind is MxCommandKind.Ping return kind is MxCommandKind.Ping
@@ -211,6 +211,7 @@ public sealed class MxGatewaySession : IAsyncDisposable
/// <param name="serverHandle">The ServerHandle from register.</param> /// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param> /// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task AdviseAsync( public async Task AdviseAsync(
int serverHandle, int serverHandle,
int itemHandle, int itemHandle,
@@ -252,6 +253,7 @@ public sealed class MxGatewaySession : IAsyncDisposable
/// <param name="serverHandle">The ServerHandle from register.</param> /// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param> /// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task UnAdviseAsync( public async Task UnAdviseAsync(
int serverHandle, int serverHandle,
int itemHandle, int itemHandle,
@@ -293,6 +295,7 @@ public sealed class MxGatewaySession : IAsyncDisposable
/// <param name="serverHandle">The ServerHandle from register.</param> /// <param name="serverHandle">The ServerHandle from register.</param>
/// <param name="itemHandle">The ItemHandle from add-item.</param> /// <param name="itemHandle">The ItemHandle from add-item.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task RemoveItemAsync( public async Task RemoveItemAsync(
int serverHandle, int serverHandle,
int itemHandle, int itemHandle,
@@ -675,6 +678,7 @@ public sealed class MxGatewaySession : IAsyncDisposable
/// <param name="value">The value to write.</param> /// <param name="value">The value to write.</param>
/// <param name="userId">User ID context for the write.</param> /// <param name="userId">User ID context for the write.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task WriteAsync( public async Task WriteAsync(
int serverHandle, int serverHandle,
int itemHandle, int itemHandle,
@@ -729,6 +733,7 @@ public sealed class MxGatewaySession : IAsyncDisposable
/// <param name="timestampValue">The timestamp to write with the value.</param> /// <param name="timestampValue">The timestamp to write with the value.</param>
/// <param name="userId">User ID context for the write.</param> /// <param name="userId">User ID context for the write.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task Write2Async( public async Task Write2Async(
int serverHandle, int serverHandle,
int itemHandle, int itemHandle,
@@ -821,6 +826,7 @@ public sealed class MxGatewaySession : IAsyncDisposable
/// <summary> /// <summary>
/// Closes the session and releases resources. /// Closes the session and releases resources.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
await CloseAsync().ConfigureAwait(false); await CloseAsync().ConfigureAwait(false);
@@ -7,6 +7,7 @@ public static class MxStatusProxyExtensions
{ {
/// <summary>Returns whether the status indicates success (success flag set and category is Ok).</summary> /// <summary>Returns whether the status indicates success (success flag set and category is Ok).</summary>
/// <param name="status">The status to check.</param> /// <param name="status">The status to check.</param>
/// <returns><c>true</c> if the status is successful; <c>false</c> otherwise.</returns>
public static bool IsSuccess(this MxStatusProxy status) public static bool IsSuccess(this MxStatusProxy status)
{ {
ArgumentNullException.ThrowIfNull(status); ArgumentNullException.ThrowIfNull(status);
@@ -17,6 +18,7 @@ public static class MxStatusProxyExtensions
/// <summary>Returns a formatted summary of the status for diagnostic output.</summary> /// <summary>Returns a formatted summary of the status for diagnostic output.</summary>
/// <param name="status">The status to summarize.</param> /// <param name="status">The status to summarize.</param>
/// <returns>A human-readable string combining category, source, detail, and diagnostic text.</returns>
public static string ToDiagnosticSummary(this MxStatusProxy status) public static string ToDiagnosticSummary(this MxStatusProxy status)
{ {
ArgumentNullException.ThrowIfNull(status); ArgumentNullException.ThrowIfNull(status);
@@ -14,6 +14,7 @@ public static class MxValueExtensions
/// Converts a boolean value to an MxValue with MxDataType.Boolean. /// Converts a boolean value to an MxValue with MxDataType.Boolean.
/// </summary> /// </summary>
/// <param name="value">Scalar boolean value to wrap.</param> /// <param name="value">Scalar boolean value to wrap.</param>
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Boolean</c>.</returns>
public static MxValue ToMxValue(this bool value) public static MxValue ToMxValue(this bool value)
{ {
return new MxValue return new MxValue
@@ -28,6 +29,7 @@ public static class MxValueExtensions
/// Converts a 32-bit integer value to an MxValue with MxDataType.Integer. /// Converts a 32-bit integer value to an MxValue with MxDataType.Integer.
/// </summary> /// </summary>
/// <param name="value">32-bit integer value to wrap.</param> /// <param name="value">32-bit integer value to wrap.</param>
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Integer</c>.</returns>
public static MxValue ToMxValue(this int value) public static MxValue ToMxValue(this int value)
{ {
return new MxValue return new MxValue
@@ -42,6 +44,7 @@ public static class MxValueExtensions
/// Converts a 64-bit integer value to an MxValue with MxDataType.Integer. /// Converts a 64-bit integer value to an MxValue with MxDataType.Integer.
/// </summary> /// </summary>
/// <param name="value">64-bit integer value to wrap.</param> /// <param name="value">64-bit integer value to wrap.</param>
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Integer</c>.</returns>
public static MxValue ToMxValue(this long value) public static MxValue ToMxValue(this long value)
{ {
return new MxValue return new MxValue
@@ -56,6 +59,7 @@ public static class MxValueExtensions
/// Converts a single-precision floating-point value to an MxValue with MxDataType.Float. /// Converts a single-precision floating-point value to an MxValue with MxDataType.Float.
/// </summary> /// </summary>
/// <param name="value">Single-precision floating-point value to wrap.</param> /// <param name="value">Single-precision floating-point value to wrap.</param>
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Float</c>.</returns>
public static MxValue ToMxValue(this float value) public static MxValue ToMxValue(this float value)
{ {
return new MxValue return new MxValue
@@ -70,6 +74,7 @@ public static class MxValueExtensions
/// Converts a double-precision floating-point value to an MxValue with MxDataType.Double. /// Converts a double-precision floating-point value to an MxValue with MxDataType.Double.
/// </summary> /// </summary>
/// <param name="value">Double-precision floating-point value to wrap.</param> /// <param name="value">Double-precision floating-point value to wrap.</param>
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Double</c>.</returns>
public static MxValue ToMxValue(this double value) public static MxValue ToMxValue(this double value)
{ {
return new MxValue return new MxValue
@@ -84,6 +89,7 @@ public static class MxValueExtensions
/// Converts a string value to an MxValue with MxDataType.String. /// Converts a string value to an MxValue with MxDataType.String.
/// </summary> /// </summary>
/// <param name="value">String value to wrap.</param> /// <param name="value">String value to wrap.</param>
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.String</c>.</returns>
public static MxValue ToMxValue(this string value) public static MxValue ToMxValue(this string value)
{ {
ArgumentNullException.ThrowIfNull(value); ArgumentNullException.ThrowIfNull(value);
@@ -100,6 +106,7 @@ public static class MxValueExtensions
/// Converts a DateTimeOffset value to an MxValue with MxDataType.Time. /// Converts a DateTimeOffset value to an MxValue with MxDataType.Time.
/// </summary> /// </summary>
/// <param name="value">DateTimeOffset value to wrap.</param> /// <param name="value">DateTimeOffset value to wrap.</param>
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Time</c>.</returns>
public static MxValue ToMxValue(this DateTimeOffset value) public static MxValue ToMxValue(this DateTimeOffset value)
{ {
return new MxValue return new MxValue
@@ -114,6 +121,7 @@ public static class MxValueExtensions
/// Converts a DateTime value to an MxValue with MxDataType.Time. /// Converts a DateTime value to an MxValue with MxDataType.Time.
/// </summary> /// </summary>
/// <param name="value">DateTime value to wrap.</param> /// <param name="value">DateTime value to wrap.</param>
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Time</c>.</returns>
public static MxValue ToMxValue(this DateTime value) public static MxValue ToMxValue(this DateTime value)
{ {
return new DateTimeOffset( return new DateTimeOffset(
@@ -127,6 +135,7 @@ public static class MxValueExtensions
/// Converts a boolean array to an MxValue with MxDataType.Boolean. /// Converts a boolean array to an MxValue with MxDataType.Boolean.
/// </summary> /// </summary>
/// <param name="values">Array of boolean values to wrap.</param> /// <param name="values">Array of boolean values to wrap.</param>
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Boolean</c> and an array payload.</returns>
public static MxValue ToMxValue(this IReadOnlyList<bool> values) public static MxValue ToMxValue(this IReadOnlyList<bool> values)
{ {
ArgumentNullException.ThrowIfNull(values); ArgumentNullException.ThrowIfNull(values);
@@ -145,6 +154,7 @@ public static class MxValueExtensions
/// Converts a 32-bit integer array to an MxValue with MxDataType.Integer. /// Converts a 32-bit integer array to an MxValue with MxDataType.Integer.
/// </summary> /// </summary>
/// <param name="values">Array of 32-bit integer values to wrap.</param> /// <param name="values">Array of 32-bit integer values to wrap.</param>
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Integer</c> and an array payload.</returns>
public static MxValue ToMxValue(this IReadOnlyList<int> values) public static MxValue ToMxValue(this IReadOnlyList<int> values)
{ {
ArgumentNullException.ThrowIfNull(values); ArgumentNullException.ThrowIfNull(values);
@@ -163,6 +173,7 @@ public static class MxValueExtensions
/// Converts a 64-bit integer array to an MxValue with MxDataType.Integer. /// Converts a 64-bit integer array to an MxValue with MxDataType.Integer.
/// </summary> /// </summary>
/// <param name="values">Array of 64-bit integer values to wrap.</param> /// <param name="values">Array of 64-bit integer values to wrap.</param>
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Integer</c> and an array payload.</returns>
public static MxValue ToMxValue(this IReadOnlyList<long> values) public static MxValue ToMxValue(this IReadOnlyList<long> values)
{ {
ArgumentNullException.ThrowIfNull(values); ArgumentNullException.ThrowIfNull(values);
@@ -181,6 +192,7 @@ public static class MxValueExtensions
/// Converts a single-precision floating-point array to an MxValue with MxDataType.Float. /// Converts a single-precision floating-point array to an MxValue with MxDataType.Float.
/// </summary> /// </summary>
/// <param name="values">Array of single-precision floating-point values to wrap.</param> /// <param name="values">Array of single-precision floating-point values to wrap.</param>
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Float</c> and an array payload.</returns>
public static MxValue ToMxValue(this IReadOnlyList<float> values) public static MxValue ToMxValue(this IReadOnlyList<float> values)
{ {
ArgumentNullException.ThrowIfNull(values); ArgumentNullException.ThrowIfNull(values);
@@ -199,6 +211,7 @@ public static class MxValueExtensions
/// Converts a double-precision floating-point array to an MxValue with MxDataType.Double. /// Converts a double-precision floating-point array to an MxValue with MxDataType.Double.
/// </summary> /// </summary>
/// <param name="values">Array of double-precision floating-point values to wrap.</param> /// <param name="values">Array of double-precision floating-point values to wrap.</param>
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Double</c> and an array payload.</returns>
public static MxValue ToMxValue(this IReadOnlyList<double> values) public static MxValue ToMxValue(this IReadOnlyList<double> values)
{ {
ArgumentNullException.ThrowIfNull(values); ArgumentNullException.ThrowIfNull(values);
@@ -217,6 +230,7 @@ public static class MxValueExtensions
/// Converts a string array to an MxValue with MxDataType.String. /// Converts a string array to an MxValue with MxDataType.String.
/// </summary> /// </summary>
/// <param name="values">Array of string values to wrap.</param> /// <param name="values">Array of string values to wrap.</param>
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.String</c> and an array payload.</returns>
public static MxValue ToMxValue(this IReadOnlyList<string> values) public static MxValue ToMxValue(this IReadOnlyList<string> values)
{ {
ArgumentNullException.ThrowIfNull(values); ArgumentNullException.ThrowIfNull(values);
@@ -235,6 +249,7 @@ public static class MxValueExtensions
/// Converts a DateTimeOffset array to an MxValue with MxDataType.Time. /// Converts a DateTimeOffset array to an MxValue with MxDataType.Time.
/// </summary> /// </summary>
/// <param name="values">Array of DateTimeOffset values to wrap.</param> /// <param name="values">Array of DateTimeOffset values to wrap.</param>
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Time</c> and an array payload.</returns>
public static MxValue ToMxValue(this IReadOnlyList<DateTimeOffset> values) public static MxValue ToMxValue(this IReadOnlyList<DateTimeOffset> values)
{ {
ArgumentNullException.ThrowIfNull(values); ArgumentNullException.ThrowIfNull(values);
@@ -253,6 +268,7 @@ public static class MxValueExtensions
/// Gets the projection kind (field name) of the given MxValue's current oneof value. /// Gets the projection kind (field name) of the given MxValue's current oneof value.
/// </summary> /// </summary>
/// <param name="value">The MxValue whose oneof projection kind is returned.</param> /// <param name="value">The MxValue whose oneof projection kind is returned.</param>
/// <returns>The JSON field name of the active oneof case, or <c>"nullValue"</c>/<c>"unspecified"</c> for null/unset values.</returns>
public static string GetProjectionKind(this MxValue value) public static string GetProjectionKind(this MxValue value)
{ {
ArgumentNullException.ThrowIfNull(value); ArgumentNullException.ThrowIfNull(value);
@@ -276,6 +292,7 @@ public static class MxValueExtensions
/// Converts an MxValue to a CLR object; returns the boxed value or null for null MxValues. /// Converts an MxValue to a CLR object; returns the boxed value or null for null MxValues.
/// </summary> /// </summary>
/// <param name="value">The MxValue to convert.</param> /// <param name="value">The MxValue to convert.</param>
/// <returns>The boxed CLR value, or null if the MxValue represents a null.</returns>
public static object? ToClrValue(this MxValue value) public static object? ToClrValue(this MxValue value)
{ {
ArgumentNullException.ThrowIfNull(value); ArgumentNullException.ThrowIfNull(value);
@@ -299,6 +316,7 @@ public static class MxValueExtensions
/// Converts an MxArray to a CLR array; returns null if the array does not have a known element type. /// Converts an MxArray to a CLR array; returns null if the array does not have a known element type.
/// </summary> /// </summary>
/// <param name="array">The MxArray to convert.</param> /// <param name="array">The MxArray to convert.</param>
/// <returns>A CLR array of the appropriate element type, or null for unknown element types.</returns>
public static object? ToClrArrayValue(this MxArray array) public static object? ToClrArrayValue(this MxArray array)
{ {
ArgumentNullException.ThrowIfNull(array); ArgumentNullException.ThrowIfNull(array);
@@ -328,6 +346,7 @@ public static class MxValueExtensions
/// <param name="variantType">Variant type string (e.g., "VT_BSTR").</param> /// <param name="variantType">Variant type string (e.g., "VT_BSTR").</param>
/// <param name="rawDiagnostic">Diagnostic string describing the raw value.</param> /// <param name="rawDiagnostic">Diagnostic string describing the raw value.</param>
/// <param name="rawDataType">Optional MXAccess data type override.</param> /// <param name="rawDataType">Optional MXAccess data type override.</param>
/// <returns>An <see cref="MxValue"/> with <c>MxDataType.Unknown</c> and the raw byte payload.</returns>
public static MxValue ToRawMxValue( public static MxValue ToRawMxValue(
byte[] value, byte[] value,
string variantType, string variantType,
@@ -14,6 +14,7 @@ namespace ZB.MOM.WW.MxGateway.IntegrationTests;
public sealed class DashboardLdapLiveTests public sealed class DashboardLdapLiveTests
{ {
/// <summary>Verifies that an admin user in the GwAdmin group authenticates successfully.</summary> /// <summary>Verifies that an admin user in the GwAdmin group authenticates successfully.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveLdapFact] [LiveLdapFact]
public async Task AuthenticateAsync_AdminInGwAdminGroup_Succeeds() public async Task AuthenticateAsync_AdminInGwAdminGroup_Succeeds()
{ {
@@ -42,6 +43,7 @@ public sealed class DashboardLdapLiveTests
} }
/// <summary>Verifies that a readonly user without GwAdmin group fails to authenticate.</summary> /// <summary>Verifies that a readonly user without GwAdmin group fails to authenticate.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveLdapFact] [LiveLdapFact]
public async Task AuthenticateAsync_ReadOnlyUserMissingGwAdminGroup_Fails() public async Task AuthenticateAsync_ReadOnlyUserMissingGwAdminGroup_Fails()
{ {
@@ -58,6 +60,7 @@ public sealed class DashboardLdapLiveTests
} }
/// <summary>Verifies that authentication with wrong password fails without leaking the password.</summary> /// <summary>Verifies that authentication with wrong password fails without leaking the password.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveLdapFact] [LiveLdapFact]
public async Task AuthenticateAsync_AdminWithWrongPassword_FailsWithoutLeakingPassword() public async Task AuthenticateAsync_AdminWithWrongPassword_FailsWithoutLeakingPassword()
{ {
@@ -77,6 +80,7 @@ public sealed class DashboardLdapLiveTests
} }
/// <summary>Verifies that authentication with unknown username fails.</summary> /// <summary>Verifies that authentication with unknown username fails.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveLdapFact] [LiveLdapFact]
public async Task AuthenticateAsync_UnknownUsername_Fails() public async Task AuthenticateAsync_UnknownUsername_Fails()
{ {
@@ -94,6 +98,7 @@ public sealed class DashboardLdapLiveTests
} }
/// <summary>Verifies that authentication fails gracefully when the server is unreachable.</summary> /// <summary>Verifies that authentication fails gracefully when the server is unreachable.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveLdapFact] [LiveLdapFact]
public async Task AuthenticateAsync_ServerUnreachable_FailsWithoutThrowing() public async Task AuthenticateAsync_ServerUnreachable_FailsWithoutThrowing()
{ {
@@ -7,6 +7,7 @@ namespace ZB.MOM.WW.MxGateway.IntegrationTests.Galaxy;
public sealed class GalaxyRepositoryLiveTests public sealed class GalaxyRepositoryLiveTests
{ {
/// <summary>Verifies that the Galaxy Repository can establish a live connection to the ZB database.</summary> /// <summary>Verifies that the Galaxy Repository can establish a live connection to the ZB database.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveGalaxyRepositoryFact] [LiveGalaxyRepositoryFact]
public async Task TestConnection_AgainstZb_Succeeds() public async Task TestConnection_AgainstZb_Succeeds()
{ {
@@ -18,6 +19,7 @@ public sealed class GalaxyRepositoryLiveTests
} }
/// <summary>Verifies that the last deploy time can be retrieved from the ZB database.</summary> /// <summary>Verifies that the last deploy time can be retrieved from the ZB database.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveGalaxyRepositoryFact] [LiveGalaxyRepositoryFact]
public async Task GetLastDeployTime_AgainstZb_ReturnsTimestamp() public async Task GetLastDeployTime_AgainstZb_ReturnsTimestamp()
{ {
@@ -29,6 +31,7 @@ public sealed class GalaxyRepositoryLiveTests
} }
/// <summary>Verifies that the hierarchy can be retrieved from the ZB database.</summary> /// <summary>Verifies that the hierarchy can be retrieved from the ZB database.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveGalaxyRepositoryFact] [LiveGalaxyRepositoryFact]
public async Task GetHierarchy_AgainstZb_ReturnsObjects() public async Task GetHierarchy_AgainstZb_ReturnsObjects()
{ {
@@ -46,6 +49,7 @@ public sealed class GalaxyRepositoryLiveTests
} }
/// <summary>Verifies that object attributes can be retrieved from the ZB database.</summary> /// <summary>Verifies that object attributes can be retrieved from the ZB database.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveGalaxyRepositoryFact] [LiveGalaxyRepositoryFact]
public async Task GetAttributes_AgainstZb_ReturnsAtLeastOneAttribute() public async Task GetAttributes_AgainstZb_ReturnsAtLeastOneAttribute()
{ {
@@ -30,6 +30,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// <summary> /// <summary>
/// Verifies that a gateway session can register, add item, advise, and stream events from live MXAccess. /// Verifies that a gateway session can register, add item, advise, and stream events from live MXAccess.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveMxAccessFact] [LiveMxAccessFact]
public async Task GatewaySession_WithLiveWorker_RegistersAdvisesStreamsDataAndCloses() public async Task GatewaySession_WithLiveWorker_RegistersAdvisesStreamsDataAndCloses()
{ {
@@ -119,6 +120,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// and that the worker emits a matching <see cref="MxEventFamily.OnWriteComplete"/> event /// and that the worker emits a matching <see cref="MxEventFamily.OnWriteComplete"/> event
/// — the proof of round-trip the cross-language client e2e runner relies on. /// — the proof of round-trip the cross-language client e2e runner relies on.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveMxAccessFact] [LiveMxAccessFact]
public async Task GatewaySession_WithLiveWorker_WritesValueToAdvisedItem() public async Task GatewaySession_WithLiveWorker_WritesValueToAdvisedItem()
{ {
@@ -235,6 +237,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// Verifies that an AddItem against an invalid server handle surfaces the MXAccess failure /// Verifies that an AddItem against an invalid server handle surfaces the MXAccess failure
/// without faulting the gateway transport, exercising the invalid-handle parity path. /// without faulting the gateway transport, exercising the invalid-handle parity path.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveMxAccessFact] [LiveMxAccessFact]
public async Task GatewaySession_WithLiveWorker_InvalidHandleCommand_SurfacesFailureWithoutTransportFault() public async Task GatewaySession_WithLiveWorker_InvalidHandleCommand_SurfacesFailureWithoutTransportFault()
{ {
@@ -293,6 +296,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// OnDataChange events for the un-advised item. Exercises the lifecycle-ordering /// OnDataChange events for the un-advised item. Exercises the lifecycle-ordering
/// parity CLAUDE.md singles out as a "do not synthesize" rule. /// parity CLAUDE.md singles out as a "do not synthesize" rule.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveMxAccessFact] [LiveMxAccessFact]
public async Task GatewaySession_WithLiveWorker_UnadviseRemoveItemUnregister_TeardownOrderingParity() public async Task GatewaySession_WithLiveWorker_UnadviseRemoveItemUnregister_TeardownOrderingParity()
{ {
@@ -437,6 +441,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// parity surface the gateway must not "fix" — the test asserts the reply kind and /// parity surface the gateway must not "fix" — the test asserts the reply kind and
/// protocol status, not a fabricated outcome. /// protocol status, not a fabricated outcome.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveMxAccessFact] [LiveMxAccessFact]
public async Task GatewaySession_WithLiveWorker_WriteSecured_AuthenticatedRoundTripParity() public async Task GatewaySession_WithLiveWorker_WriteSecured_AuthenticatedRoundTripParity()
{ {
@@ -568,6 +573,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// must observe the abnormal exit, transition the session, and surface a non-empty /// must observe the abnormal exit, transition the session, and surface a non-empty
/// fault description rather than hanging or crashing. /// fault description rather than hanging or crashing.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[LiveMxAccessFact] [LiveMxAccessFact]
public async Task GatewaySession_WithLiveWorker_AbnormalWorkerExit_MarksSessionFaulted() public async Task GatewaySession_WithLiveWorker_AbnormalWorkerExit_MarksSessionFaulted()
{ {
@@ -1114,6 +1120,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// </summary> /// </summary>
/// <param name="sessionId">The session identifier.</param> /// <param name="sessionId">The session identifier.</param>
/// <param name="session">The session if found; otherwise null.</param> /// <param name="session">The session if found; otherwise null.</param>
/// <returns>True if the session was found; otherwise false.</returns>
public bool TryGetSession(string sessionId, [MaybeNullWhen(false)] out GatewaySession session) public bool TryGetSession(string sessionId, [MaybeNullWhen(false)] out GatewaySession session)
{ {
return _registry.TryGet(sessionId, out session); return _registry.TryGet(sessionId, out session);
@@ -1122,6 +1129,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// <summary> /// <summary>
/// Disposes the fixture resources and closes all sessions. /// Disposes the fixture resources and closes all sessions.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous dispose operation.</returns>
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
foreach (GatewaySession session in _registry.Snapshot()) foreach (GatewaySession session in _registry.Snapshot())
@@ -1192,6 +1200,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// Records the message and signals any pending waiter. /// Records the message and signals any pending waiter.
/// </summary> /// </summary>
/// <param name="message">The message to write.</param> /// <param name="message">The message to write.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task WriteAsync(T message) public Task WriteAsync(T message)
{ {
lock (syncRoot) lock (syncRoot)
@@ -1374,7 +1383,9 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
return workerProcess; return workerProcess;
} }
/// <inheritdoc /> /// <summary>Waits for all recorded worker processes to exit within the specified timeout.</summary>
/// <param name="timeout">Maximum time to wait for each process to exit.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task WaitForProcessesAsync(TimeSpan timeout) public async Task WaitForProcessesAsync(TimeSpan timeout)
{ {
foreach (TestWorkerProcess process in processes) foreach (TestWorkerProcess process in processes)
@@ -1454,7 +1465,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
process.Kill(entireProcessTree); process.Kill(entireProcessTree);
} }
/// <inheritdoc /> /// <summary>Releases the wrapped process resources.</summary>
public void Dispose() public void Dispose()
{ {
process.Dispose(); process.Dispose();
@@ -1466,13 +1477,15 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// </summary> /// </summary>
private sealed class TestOutputLoggerProvider(ITestOutputHelper output) : ILoggerProvider private sealed class TestOutputLoggerProvider(ITestOutputHelper output) : ILoggerProvider
{ {
/// <inheritdoc /> /// <summary>Creates a logger that writes to the test output helper for the given category.</summary>
/// <param name="categoryName">The logger category name.</param>
/// <returns>A logger that forwards to the test output helper.</returns>
public ILogger CreateLogger(string categoryName) public ILogger CreateLogger(string categoryName)
{ {
return new TestOutputLogger(output, categoryName); return new TestOutputLogger(output, categoryName);
} }
/// <inheritdoc /> /// <summary>Releases resources held by the provider (no-op for this test double).</summary>
public void Dispose() public void Dispose()
{ {
} }
@@ -1485,20 +1498,31 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
ITestOutputHelper output, ITestOutputHelper output,
string categoryName) : ILogger string categoryName) : ILogger
{ {
/// <inheritdoc /> /// <summary>Begins a log scope; returns null as this test logger does not support scopes.</summary>
/// <param name="state">The state object for the scope.</param>
/// <typeparam name="TState">The type of the state object.</typeparam>
/// <returns>Always null.</returns>
public IDisposable? BeginScope<TState>(TState state) public IDisposable? BeginScope<TState>(TState state)
where TState : notnull where TState : notnull
{ {
return null; return null;
} }
/// <inheritdoc /> /// <summary>Returns true for log levels at or above <see cref="LogLevel.Information"/>.</summary>
/// <param name="logLevel">The log level to check.</param>
/// <returns>True if the log level is enabled.</returns>
public bool IsEnabled(LogLevel logLevel) public bool IsEnabled(LogLevel logLevel)
{ {
return logLevel >= LogLevel.Information; return logLevel >= LogLevel.Information;
} }
/// <inheritdoc /> /// <summary>Writes a log entry to the test output helper.</summary>
/// <param name="logLevel">The log level.</param>
/// <param name="eventId">The event identifier.</param>
/// <param name="state">The state object to log.</param>
/// <param name="exception">Optional exception associated with the log entry.</param>
/// <param name="formatter">Function to format the state and exception into a string.</param>
/// <typeparam name="TState">The type of the state object.</typeparam>
public void Log<TState>( public void Log<TState>(
LogLevel logLevel, LogLevel logLevel,
EventId eventId, EventId eventId,
@@ -688,6 +688,7 @@ public sealed class GatewayAlarmMonitor : BackgroundService, IGatewayAlarmServic
/// <summary>Determines whether the alarm reference matches this subscriber's filter.</summary> /// <summary>Determines whether the alarm reference matches this subscriber's filter.</summary>
/// <param name="reference">The alarm reference to match.</param> /// <param name="reference">The alarm reference to match.</param>
/// <returns>True if the reference starts with this subscriber's prefix or no prefix is set.</returns>
public bool Matches(string reference) public bool Matches(string reference)
{ {
return prefix.Length == 0 || reference.StartsWith(prefix, StringComparison.Ordinal); return prefix.Length == 0 || reference.StartsWith(prefix, StringComparison.Ordinal);
@@ -46,6 +46,7 @@ public interface IGatewayAlarmService
/// </summary> /// </summary>
/// <param name="alarmFilterPrefix">Optional alarm-reference prefix scoping the feed.</param> /// <param name="alarmFilterPrefix">Optional alarm-reference prefix scoping the feed.</param>
/// <param name="cancellationToken">Token that ends the subscription.</param> /// <param name="cancellationToken">Token that ends the subscription.</param>
/// <returns>An async enumerable of alarm feed messages.</returns>
IAsyncEnumerable<AlarmFeedMessage> StreamAsync( IAsyncEnumerable<AlarmFeedMessage> StreamAsync(
string? alarmFilterPrefix, string? alarmFilterPrefix,
CancellationToken cancellationToken); CancellationToken cancellationToken);
@@ -57,6 +58,7 @@ public interface IGatewayAlarmService
/// </summary> /// </summary>
/// <param name="request">The acknowledge request.</param> /// <param name="request">The acknowledge request.</param>
/// <param name="cancellationToken">Token to cancel the call.</param> /// <param name="cancellationToken">Token to cancel the call.</param>
/// <returns>A task that resolves to the acknowledge reply.</returns>
Task<AcknowledgeAlarmReply> AcknowledgeAsync( Task<AcknowledgeAlarmReply> AcknowledgeAsync(
AcknowledgeAlarmRequest request, AcknowledgeAlarmRequest request,
CancellationToken cancellationToken); CancellationToken cancellationToken);
@@ -9,11 +9,7 @@ public sealed class GatewayOptionsValidator : OptionsValidatorBase<GatewayOption
private const int MinimumMaxMessageBytes = 1024; private const int MinimumMaxMessageBytes = 1024;
private const int MaximumMaxMessageBytes = 256 * 1024 * 1024; private const int MaximumMaxMessageBytes = 256 * 1024 * 1024;
/// <summary> /// <inheritdoc />
/// Validates gateway configuration options.
/// </summary>
/// <param name="builder">The accumulator to record failures on.</param>
/// <param name="options">Gateway options to validate.</param>
protected override void Validate(ValidationBuilder builder, GatewayOptions options) protected override void Validate(ValidationBuilder builder, GatewayOptions options)
{ {
ValidateAuthentication(options.Authentication, builder); ValidateAuthentication(options.Authentication, builder);
@@ -8,5 +8,6 @@ public interface IGatewayConfigurationProvider
/// <summary> /// <summary>
/// Returns the validated and effective gateway configuration. /// Returns the validated and effective gateway configuration.
/// </summary> /// </summary>
/// <returns>The <see cref="EffectiveGatewayConfiguration"/> with validated defaults applied.</returns>
EffectiveGatewayConfiguration GetEffectiveConfiguration(); EffectiveGatewayConfiguration GetEffectiveConfiguration();
} }
@@ -38,7 +38,8 @@ public abstract class DashboardPageBase : ComponentBase, IAsyncDisposable
await ConnectHubAsync().ConfigureAwait(false); await ConnectHubAsync().ConfigureAwait(false);
} }
/// <inheritdoc /> /// <summary>Disposes the SignalR hub connection and suppresses finalization.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
if (_hub is not null) if (_hub is not null)
@@ -6,6 +6,7 @@ public sealed class DashboardApiKeyAuthorization
{ {
/// <summary>Determines whether the user can manage API keys.</summary> /// <summary>Determines whether the user can manage API keys.</summary>
/// <param name="user">The authenticated user principal.</param> /// <param name="user">The authenticated user principal.</param>
/// <returns>True if the user is an authenticated admin; otherwise false.</returns>
public bool CanManage(ClaimsPrincipal user) public bool CanManage(ClaimsPrincipal user)
{ {
if (user.Identity?.IsAuthenticated != true) if (user.Identity?.IsAuthenticated != true)
@@ -20,17 +20,13 @@ public sealed class DashboardApiKeyManagementService(
private const string UnauthorizedMessage = "Sign in with an authorized LDAP account to manage API keys."; private const string UnauthorizedMessage = "Sign in with an authorized LDAP account to manage API keys.";
private const string PepperUnavailableMarker = "pepper unavailable"; private const string PepperUnavailableMarker = "pepper unavailable";
/// <summary>Determines whether the user can manage API keys.</summary> /// <inheritdoc />
/// <param name="user">The authenticated user principal.</param>
public bool CanManage(ClaimsPrincipal user) public bool CanManage(ClaimsPrincipal user)
{ {
return authorization.CanManage(user); return authorization.CanManage(user);
} }
/// <summary>Creates an API key asynchronously.</summary> /// <inheritdoc />
/// <param name="user">The authenticated user principal.</param>
/// <param name="request">The request payload.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param>
public async Task<DashboardApiKeyManagementResult> CreateAsync( public async Task<DashboardApiKeyManagementResult> CreateAsync(
ClaimsPrincipal user, ClaimsPrincipal user,
DashboardApiKeyManagementRequest request, DashboardApiKeyManagementRequest request,
@@ -82,10 +78,7 @@ public sealed class DashboardApiKeyManagementService(
} }
} }
/// <summary>Revokes an API key asynchronously.</summary> /// <inheritdoc />
/// <param name="user">The authenticated user principal.</param>
/// <param name="keyId">The API key identifier.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param>
public async Task<DashboardApiKeyManagementResult> RevokeAsync( public async Task<DashboardApiKeyManagementResult> RevokeAsync(
ClaimsPrincipal user, ClaimsPrincipal user,
string keyId, string keyId,
@@ -120,10 +113,7 @@ public sealed class DashboardApiKeyManagementService(
: DashboardApiKeyManagementResult.Fail("API key was not found or is already revoked."); : DashboardApiKeyManagementResult.Fail("API key was not found or is already revoked.");
} }
/// <summary>Rotates an API key secret asynchronously.</summary> /// <inheritdoc />
/// <param name="user">The authenticated user principal.</param>
/// <param name="keyId">The API key identifier.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param>
public async Task<DashboardApiKeyManagementResult> RotateAsync( public async Task<DashboardApiKeyManagementResult> RotateAsync(
ClaimsPrincipal user, ClaimsPrincipal user,
string keyId, string keyId,
@@ -170,10 +160,7 @@ public sealed class DashboardApiKeyManagementService(
} }
} }
/// <summary>Deletes a revoked API key asynchronously.</summary> /// <inheritdoc />
/// <param name="user">The authenticated user principal.</param>
/// <param name="keyId">The API key identifier.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param>
public async Task<DashboardApiKeyManagementResult> DeleteAsync( public async Task<DashboardApiKeyManagementResult> DeleteAsync(
ClaimsPrincipal user, ClaimsPrincipal user,
string keyId, string keyId,
@@ -23,6 +23,7 @@ public sealed record DashboardAuthenticationResult(
/// Creates a successful authentication result. /// Creates a successful authentication result.
/// </summary> /// </summary>
/// <param name="principal">Authenticated principal.</param> /// <param name="principal">Authenticated principal.</param>
/// <returns>A successful <see cref="DashboardAuthenticationResult"/> wrapping the principal.</returns>
public static DashboardAuthenticationResult Success(ClaimsPrincipal principal) public static DashboardAuthenticationResult Success(ClaimsPrincipal principal)
{ {
return new DashboardAuthenticationResult(true, principal, null); return new DashboardAuthenticationResult(true, principal, null);
@@ -32,6 +33,7 @@ public sealed record DashboardAuthenticationResult(
/// Creates a failed authentication result. /// Creates a failed authentication result.
/// </summary> /// </summary>
/// <param name="failureMessage">Diagnostic message describing the failure.</param> /// <param name="failureMessage">Diagnostic message describing the failure.</param>
/// <returns>A failed <see cref="DashboardAuthenticationResult"/> with the given message.</returns>
public static DashboardAuthenticationResult Fail(string failureMessage) public static DashboardAuthenticationResult Fail(string failureMessage)
{ {
return new DashboardAuthenticationResult(false, null, failureMessage); return new DashboardAuthenticationResult(false, null, failureMessage);
@@ -6,6 +6,7 @@ public static class DashboardConnectionStringDisplay
{ {
/// <summary>Returns a sanitized Galaxy Repository connection string for display.</summary> /// <summary>Returns a sanitized Galaxy Repository connection string for display.</summary>
/// <param name="connectionString">The connection string to sanitize.</param> /// <param name="connectionString">The connection string to sanitize.</param>
/// <returns>A sanitized connection string with credentials removed, or <c>"[invalid connection string]"</c> if parsing fails.</returns>
public static string GalaxyRepositoryConnectionString(string connectionString) public static string GalaxyRepositoryConnectionString(string connectionString)
{ {
try try
@@ -7,6 +7,7 @@ internal static class DashboardGalaxyProjector
{ {
/// <summary>Projects the cache entry to a dashboard Galaxy summary.</summary> /// <summary>Projects the cache entry to a dashboard Galaxy summary.</summary>
/// <param name="entry">The Galaxy hierarchy cache entry.</param> /// <param name="entry">The Galaxy hierarchy cache entry.</param>
/// <returns>The precomputed <see cref="DashboardGalaxySummary"/> from the cache entry.</returns>
public static DashboardGalaxySummary Project(GalaxyHierarchyCacheEntry entry) public static DashboardGalaxySummary Project(GalaxyHierarchyCacheEntry entry)
{ {
return entry.DashboardSummary; return entry.DashboardSummary;
@@ -17,7 +17,10 @@ namespace ZB.MOM.WW.MxGateway.Server.Dashboard;
public sealed class DashboardGroupRoleMapper(IOptions<GatewayOptions> options) public sealed class DashboardGroupRoleMapper(IOptions<GatewayOptions> options)
: IGroupRoleMapper<string> : IGroupRoleMapper<string>
{ {
/// <inheritdoc /> /// <summary>Maps LDAP group memberships to dashboard roles using the configured group-to-role rules.</summary>
/// <param name="groups">The list of LDAP group names or distinguished names for the authenticated user.</param>
/// <param name="ct">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the role mapping for the supplied groups.</returns>
public Task<GroupRoleMapping<string>> MapAsync( public Task<GroupRoleMapping<string>> MapAsync(
IReadOnlyList<string> groups, IReadOnlyList<string> groups,
CancellationToken ct) CancellationToken ct)
@@ -16,6 +16,7 @@ internal static class DashboardGroupRoleMapping
/// </summary> /// </summary>
/// <param name="groups">The collection of LDAP groups the user belongs to.</param> /// <param name="groups">The collection of LDAP groups the user belongs to.</param>
/// <param name="groupToRole">The mapping from group names to dashboard role names.</param> /// <param name="groupToRole">The mapping from group names to dashboard role names.</param>
/// <returns>The distinct set of dashboard roles matched from the user's groups.</returns>
internal static IReadOnlyList<string> MapGroupsToRoles( internal static IReadOnlyList<string> MapGroupsToRoles(
IEnumerable<string> groups, IEnumerable<string> groups,
IReadOnlyDictionary<string, string> groupToRole) IReadOnlyDictionary<string, string> groupToRole)
@@ -61,6 +62,7 @@ internal static class DashboardGroupRoleMapping
/// <summary>Extracts the first RDN value from a distinguished name.</summary> /// <summary>Extracts the first RDN value from a distinguished name.</summary>
/// <param name="distinguishedName">The LDAP distinguished name.</param> /// <param name="distinguishedName">The LDAP distinguished name.</param>
/// <returns>The value portion of the first RDN component, or the full string if no <c>=</c> is found.</returns>
internal static string ExtractFirstRdnValue(string distinguishedName) internal static string ExtractFirstRdnValue(string distinguishedName)
{ {
int equalsIndex = distinguishedName.IndexOf('='); int equalsIndex = distinguishedName.IndexOf('=');
@@ -192,7 +192,8 @@ public sealed class DashboardLiveDataService : IDashboardLiveDataService, IAsync
} }
} }
/// <inheritdoc /> /// <summary>Releases resources and closes the associated gateway session.</summary>
/// <returns>A task that represents the asynchronous dispose operation.</returns>
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
if (_disposed) if (_disposed)
@@ -23,6 +23,7 @@ public static class DashboardServiceCollectionExtensions
/// Application configuration, used to bind the shared LDAP provider's options /// Application configuration, used to bind the shared LDAP provider's options
/// from the <c>MxGateway:Ldap</c> section. /// from the <c>MxGateway:Ldap</c> section.
/// </param> /// </param>
/// <returns>The <paramref name="services"/> collection for chaining.</returns>
public static IServiceCollection AddGatewayDashboard( public static IServiceCollection AddGatewayDashboard(
this IServiceCollection services, this IServiceCollection services,
IConfiguration configuration) IConfiguration configuration)
@@ -6,6 +6,7 @@ public sealed record DashboardSessionAdminResult(
{ {
/// <summary>Creates a successful result with the given message.</summary> /// <summary>Creates a successful result with the given message.</summary>
/// <param name="message">The result message.</param> /// <param name="message">The result message.</param>
/// <returns>A <see cref="DashboardSessionAdminResult"/> with <c>Succeeded</c> set to <c>true</c>.</returns>
public static DashboardSessionAdminResult Success(string message) public static DashboardSessionAdminResult Success(string message)
{ {
return new DashboardSessionAdminResult(true, message); return new DashboardSessionAdminResult(true, message);
@@ -13,6 +14,7 @@ public sealed record DashboardSessionAdminResult(
/// <summary>Creates a failed result with the given message.</summary> /// <summary>Creates a failed result with the given message.</summary>
/// <param name="message">The result message.</param> /// <param name="message">The result message.</param>
/// <returns>A <see cref="DashboardSessionAdminResult"/> with <c>Succeeded</c> set to <c>false</c>.</returns>
public static DashboardSessionAdminResult Fail(string message) public static DashboardSessionAdminResult Fail(string message)
{ {
return new DashboardSessionAdminResult(false, message); return new DashboardSessionAdminResult(false, message);
@@ -65,10 +65,7 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
_logger = logger ?? NullLogger<DashboardSnapshotService>.Instance; _logger = logger ?? NullLogger<DashboardSnapshotService>.Instance;
} }
/// <summary> /// <inheritdoc />
/// Gets a current dashboard snapshot of gateway state.
/// </summary>
/// <returns>Dashboard snapshot.</returns>
public DashboardSnapshot GetSnapshot() public DashboardSnapshot GetSnapshot()
{ {
DateTimeOffset generatedAt = _timeProvider.GetUtcNow(); DateTimeOffset generatedAt = _timeProvider.GetUtcNow();
@@ -100,11 +97,7 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
Galaxy: DashboardGalaxyProjector.Project(_galaxyHierarchyCache.Current)); Galaxy: DashboardGalaxyProjector.Project(_galaxyHierarchyCache.Current));
} }
/// <summary> /// <inheritdoc />
/// Watches dashboard snapshots at regular intervals asynchronously.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Async enumerable of dashboard snapshots.</returns>
public async IAsyncEnumerable<DashboardSnapshot> WatchSnapshotsAsync( public async IAsyncEnumerable<DashboardSnapshot> WatchSnapshotsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken) [EnumeratorCancellation] CancellationToken cancellationToken)
{ {
@@ -40,6 +40,7 @@ public sealed class HubTokenService
/// <summary>Issues a bearer token carrying the user's identity and roles.</summary> /// <summary>Issues a bearer token carrying the user's identity and roles.</summary>
/// <param name="user">The claims principal representing the user.</param> /// <param name="user">The claims principal representing the user.</param>
/// <returns>The time-limited bearer token string.</returns>
public string Issue(ClaimsPrincipal user) public string Issue(ClaimsPrincipal user)
{ {
ArgumentNullException.ThrowIfNull(user); ArgumentNullException.ThrowIfNull(user);
@@ -52,6 +53,7 @@ public sealed class HubTokenService
/// <summary>Validates a token and returns the equivalent claims principal; null when invalid or expired.</summary> /// <summary>Validates a token and returns the equivalent claims principal; null when invalid or expired.</summary>
/// <param name="token">The token string to validate.</param> /// <param name="token">The token string to validate.</param>
/// <returns>The claims principal if the token is valid, or null if invalid or expired.</returns>
public ClaimsPrincipal? Validate(string? token) public ClaimsPrincipal? Validate(string? token)
{ {
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
@@ -14,9 +14,7 @@ public sealed class DashboardEventBroadcaster(
IHubContext<EventsHub> hubContext, IHubContext<EventsHub> hubContext,
ILogger<DashboardEventBroadcaster> logger) : IDashboardEventBroadcaster ILogger<DashboardEventBroadcaster> logger) : IDashboardEventBroadcaster
{ {
/// <summary>Publishes an MX event to connected dashboard clients.</summary> /// <inheritdoc />
/// <param name="sessionId">The session identifier.</param>
/// <param name="mxEvent">The MX event to publish.</param>
public void Publish(string sessionId, MxEvent mxEvent) public void Publish(string sessionId, MxEvent mxEvent)
{ {
if (string.IsNullOrEmpty(sessionId) || mxEvent is null) if (string.IsNullOrEmpty(sessionId) || mxEvent is null)
@@ -49,6 +49,7 @@ public sealed class EventsHub : Hub
/// dedicated authorization policy applied to the hub method itself. /// dedicated authorization policy applied to the hub method itself.
/// </remarks> /// </remarks>
/// <param name="sessionId">Session id to subscribe the caller to.</param> /// <param name="sessionId">Session id to subscribe the caller to.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task SubscribeSession(string sessionId) public Task SubscribeSession(string sessionId)
{ {
if (string.IsNullOrWhiteSpace(sessionId)) if (string.IsNullOrWhiteSpace(sessionId))
@@ -11,6 +11,7 @@ public interface IDashboardAuthenticator
/// <param name="username">Username to authenticate.</param> /// <param name="username">Username to authenticate.</param>
/// <param name="password">Password to authenticate.</param> /// <param name="password">Password to authenticate.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the authentication result.</returns>
Task<DashboardAuthenticationResult> AuthenticateAsync( Task<DashboardAuthenticationResult> AuthenticateAsync(
string? username, string? username,
string? password, string? password,
@@ -12,11 +12,13 @@ public interface IDashboardBrowseService
{ {
/// <summary>Returns root browse nodes (objects with no parent).</summary> /// <summary>Returns root browse nodes (objects with no parent).</summary>
/// <param name="filter">Filter arguments forwarded to the projector.</param> /// <param name="filter">Filter arguments forwarded to the projector.</param>
/// <returns>The root-level browse result.</returns>
BrowseLevelResult GetRoots(BrowseFilterArgs filter); BrowseLevelResult GetRoots(BrowseFilterArgs filter);
/// <summary>Returns the direct children of the given parent gobject id.</summary> /// <summary>Returns the direct children of the given parent gobject id.</summary>
/// <param name="parentGobjectId">The Galaxy gobject id of the parent to expand.</param> /// <param name="parentGobjectId">The Galaxy gobject id of the parent to expand.</param>
/// <param name="filter">Filter arguments forwarded to the projector.</param> /// <param name="filter">Filter arguments forwarded to the projector.</param>
/// <returns>The children browse result for the specified parent.</returns>
BrowseLevelResult GetChildren(int parentGobjectId, BrowseFilterArgs filter); BrowseLevelResult GetChildren(int parentGobjectId, BrowseFilterArgs filter);
/// <summary>Current Galaxy cache sequence. Bumps after each successful refresh.</summary> /// <summary>Current Galaxy cache sequence. Bumps after each successful refresh.</summary>
@@ -8,11 +8,13 @@ public interface IDashboardSnapshotService
/// <summary> /// <summary>
/// Gets the current dashboard snapshot. /// Gets the current dashboard snapshot.
/// </summary> /// </summary>
/// <returns>The most recent <see cref="DashboardSnapshot"/>.</returns>
DashboardSnapshot GetSnapshot(); DashboardSnapshot GetSnapshot();
/// <summary> /// <summary>
/// Watches for changes to the dashboard state as an async enumerable. /// Watches for changes to the dashboard state as an async enumerable.
/// </summary> /// </summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>An async sequence of <see cref="DashboardSnapshot"/> values as state changes.</returns>
IAsyncEnumerable<DashboardSnapshot> WatchSnapshotsAsync(CancellationToken cancellationToken); IAsyncEnumerable<DashboardSnapshot> WatchSnapshotsAsync(CancellationToken cancellationToken);
} }
@@ -12,9 +12,15 @@ public sealed class AuthStoreHealthCheck : IHealthCheck
{ {
private readonly AuthSqliteConnectionFactory _connectionFactory; private readonly AuthSqliteConnectionFactory _connectionFactory;
/// <summary>Initializes a new instance of <see cref="AuthStoreHealthCheck"/> with the given connection factory.</summary>
/// <param name="connectionFactory">Factory for opening SQLite connections to the auth store.</param>
public AuthStoreHealthCheck(AuthSqliteConnectionFactory connectionFactory) => public AuthStoreHealthCheck(AuthSqliteConnectionFactory connectionFactory) =>
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
/// <summary>Runs a lightweight connectivity probe against the SQLite authentication store.</summary>
/// <param name="context">Health check context supplied by the framework.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the health check result.</returns>
public async Task<HealthCheckResult> CheckHealthAsync( public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, HealthCheckContext context,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
@@ -19,6 +19,7 @@ public static class GatewayLogRedactor
/// Determines whether a command method bears credentials. /// Determines whether a command method bears credentials.
/// </summary> /// </summary>
/// <param name="commandMethod">The command method name to check.</param> /// <param name="commandMethod">The command method name to check.</param>
/// <returns><c>true</c> if the method carries credentials; otherwise <c>false</c>.</returns>
public static bool IsCredentialBearingCommand(string? commandMethod) public static bool IsCredentialBearingCommand(string? commandMethod)
{ {
return commandMethod is not null return commandMethod is not null
@@ -29,6 +30,7 @@ public static class GatewayLogRedactor
/// Redacts the API key secret portion of a Bearer authorization header. /// Redacts the API key secret portion of a Bearer authorization header.
/// </summary> /// </summary>
/// <param name="authorizationHeader">The authorization header value to redact.</param> /// <param name="authorizationHeader">The authorization header value to redact.</param>
/// <returns>The header with the secret portion replaced by <see cref="RedactedValue"/>, or the original if no key is detected.</returns>
public static string? RedactApiKey(string? authorizationHeader) public static string? RedactApiKey(string? authorizationHeader)
{ {
if (string.IsNullOrWhiteSpace(authorizationHeader)) if (string.IsNullOrWhiteSpace(authorizationHeader))
@@ -62,6 +64,7 @@ public static class GatewayLogRedactor
/// Redacts the client identity if it contains an API key. /// Redacts the client identity if it contains an API key.
/// </summary> /// </summary>
/// <param name="clientIdentity">The client identity string to redact.</param> /// <param name="clientIdentity">The client identity string to redact.</param>
/// <returns>The redacted identity string, or the original if no key pattern is found.</returns>
public static string? RedactClientIdentity(string? clientIdentity) public static string? RedactClientIdentity(string? clientIdentity)
{ {
if (string.IsNullOrWhiteSpace(clientIdentity)) if (string.IsNullOrWhiteSpace(clientIdentity))
@@ -80,6 +83,7 @@ public static class GatewayLogRedactor
/// <param name="commandMethod">The command method name to check for credentials.</param> /// <param name="commandMethod">The command method name to check for credentials.</param>
/// <param name="value">The command value to redact.</param> /// <param name="value">The command value to redact.</param>
/// <param name="valueLoggingEnabled">Whether value logging is enabled.</param> /// <param name="valueLoggingEnabled">Whether value logging is enabled.</param>
/// <returns>The original value when logging is enabled and the command is not credential-bearing; otherwise <see cref="RedactedValue"/>.</returns>
public static object? RedactCommandValue( public static object? RedactCommandValue(
string? commandMethod, string? commandMethod,
object? value, object? value,
@@ -8,6 +8,7 @@ public sealed record GatewayLogScope(
string? ClientIdentity = null) string? ClientIdentity = null)
{ {
/// <summary>Converts the log scope to a dictionary with redacted sensitive fields.</summary> /// <summary>Converts the log scope to a dictionary with redacted sensitive fields.</summary>
/// <returns>A dictionary of non-null scope properties with sensitive fields redacted.</returns>
public IReadOnlyDictionary<string, object?> ToDictionary() public IReadOnlyDictionary<string, object?> ToDictionary()
{ {
Dictionary<string, object?> values = []; Dictionary<string, object?> values = [];
@@ -19,6 +19,7 @@ public static class GatewayRequestLoggingMiddlewareExtensions
/// <summary>Adds gateway request logging scope middleware that reads correlation headers and redacts sensitive data.</summary> /// <summary>Adds gateway request logging scope middleware that reads correlation headers and redacts sensitive data.</summary>
/// <param name="app">Application builder.</param> /// <param name="app">Application builder.</param>
/// <returns>The <paramref name="app"/> instance for chaining.</returns>
public static IApplicationBuilder UseGatewayRequestLoggingScope(this IApplicationBuilder app) public static IApplicationBuilder UseGatewayRequestLoggingScope(this IApplicationBuilder app)
{ {
ArgumentNullException.ThrowIfNull(app); ArgumentNullException.ThrowIfNull(app);
@@ -27,6 +27,7 @@ public static class GalaxyBrowseProjector
/// <param name="browseSubtreeGlobs">Optional API-key browse-subtree constraints.</param> /// <param name="browseSubtreeGlobs">Optional API-key browse-subtree constraints.</param>
/// <param name="offset">Zero-based offset into the filtered child list.</param> /// <param name="offset">Zero-based offset into the filtered child list.</param>
/// <param name="pageSize">Maximum number of children to return.</param> /// <param name="pageSize">Maximum number of children to return.</param>
/// <returns>A page of children with total count and filter signature.</returns>
public static GalaxyBrowseChildrenResult ProjectChildren( public static GalaxyBrowseChildrenResult ProjectChildren(
GalaxyHierarchyCacheEntry entry, GalaxyHierarchyCacheEntry entry,
BrowseChildrenRequest request, BrowseChildrenRequest request,
@@ -71,6 +72,7 @@ public static class GalaxyBrowseProjector
/// </summary> /// </summary>
/// <param name="entry">The Galaxy hierarchy cache entry to query.</param> /// <param name="entry">The Galaxy hierarchy cache entry to query.</param>
/// <param name="request">The browse-children request.</param> /// <param name="request">The browse-children request.</param>
/// <returns>The resolved parent gobject id, or 0 for roots.</returns>
public static int ResolveParentId(GalaxyHierarchyCacheEntry entry, BrowseChildrenRequest request) public static int ResolveParentId(GalaxyHierarchyCacheEntry entry, BrowseChildrenRequest request)
{ {
switch (request.ParentCase) switch (request.ParentCase)
@@ -257,6 +259,7 @@ public static class GalaxyBrowseProjector
/// <param name="request">The browse-children request.</param> /// <param name="request">The browse-children request.</param>
/// <param name="browseSubtreeGlobs">Optional API-key browse-subtree constraints.</param> /// <param name="browseSubtreeGlobs">Optional API-key browse-subtree constraints.</param>
/// <param name="parentId">Resolved parent gobject id (0 for roots).</param> /// <param name="parentId">Resolved parent gobject id (0 for roots).</param>
/// <returns>A hex-encoded SHA-256 prefix that uniquely identifies the filter combination.</returns>
public static string ComputeFilterSignature( public static string ComputeFilterSignature(
BrowseChildrenRequest request, BrowseChildrenRequest request,
IReadOnlyList<string>? browseSubtreeGlobs, IReadOnlyList<string>? browseSubtreeGlobs,
@@ -20,9 +20,7 @@ public sealed class GalaxyDeployNotifier : IGalaxyDeployNotifier
private readonly ConcurrentDictionary<Guid, Channel<GalaxyDeployEventInfo>> _subscribers = new(); private readonly ConcurrentDictionary<Guid, Channel<GalaxyDeployEventInfo>> _subscribers = new();
private GalaxyDeployEventInfo? _latest; private GalaxyDeployEventInfo? _latest;
/// <summary> /// <inheritdoc />
/// The most recent deploy event, or null if none has been published.
/// </summary>
public GalaxyDeployEventInfo? Latest => Volatile.Read(ref _latest); public GalaxyDeployEventInfo? Latest => Volatile.Read(ref _latest);
/// <inheritdoc /> /// <inheritdoc />
@@ -46,6 +46,7 @@ public static class GalaxyGlobMatcher
/// <summary>Determines whether a value matches a glob pattern (with * and ? wildcards).</summary> /// <summary>Determines whether a value matches a glob pattern (with * and ? wildcards).</summary>
/// <param name="value">The value to test against the glob pattern.</param> /// <param name="value">The value to test against the glob pattern.</param>
/// <param name="glob">The glob pattern with * and ? wildcards.</param> /// <param name="glob">The glob pattern with * and ? wildcards.</param>
/// <returns><see langword="true"/> if the value matches the glob pattern; otherwise <see langword="false"/>.</returns>
public static bool IsMatch(string value, string glob) public static bool IsMatch(string value, string glob)
{ {
if (string.IsNullOrWhiteSpace(glob)) if (string.IsNullOrWhiteSpace(glob))
@@ -54,7 +54,7 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
_snapshotStore = snapshotStore; _snapshotStore = snapshotStore;
} }
/// <summary>Gets the current Galaxy hierarchy cache entry with projected status.</summary> /// <inheritdoc />
public GalaxyHierarchyCacheEntry Current public GalaxyHierarchyCacheEntry Current
{ {
get get
@@ -74,9 +74,7 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
} }
} }
/// <summary>Refreshes the Galaxy hierarchy cache if the deploy time has advanced.</summary> /// <inheritdoc />
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>Asynchronous task representing the refresh operation.</returns>
public async Task RefreshAsync(CancellationToken cancellationToken) public async Task RefreshAsync(CancellationToken cancellationToken)
{ {
await _refreshGate.WaitAsync(cancellationToken).ConfigureAwait(false); await _refreshGate.WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -90,9 +88,7 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
} }
} }
/// <summary>Waits for the Galaxy hierarchy cache to complete its first load.</summary> /// <inheritdoc />
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>Asynchronous task representing the wait operation.</returns>
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) public Task WaitForFirstLoadAsync(CancellationToken cancellationToken)
{ {
return _firstLoad.Task.WaitAsync(cancellationToken); return _firstLoad.Task.WaitAsync(cancellationToken);
@@ -25,6 +25,7 @@ public static class GalaxyHierarchyProjector
/// <param name="entry">The Galaxy hierarchy cache entry.</param> /// <param name="entry">The Galaxy hierarchy cache entry.</param>
/// <param name="request">The discovery hierarchy request.</param> /// <param name="request">The discovery hierarchy request.</param>
/// <param name="browseSubtreeGlobs">Optional glob patterns to filter browse subtrees.</param> /// <param name="browseSubtreeGlobs">Optional glob patterns to filter browse subtrees.</param>
/// <returns>The query result containing matching objects.</returns>
public static GalaxyHierarchyQueryResult Project( public static GalaxyHierarchyQueryResult Project(
GalaxyHierarchyCacheEntry entry, GalaxyHierarchyCacheEntry entry,
DiscoverHierarchyRequest request, DiscoverHierarchyRequest request,
@@ -44,6 +45,7 @@ public static class GalaxyHierarchyProjector
/// <param name="browseSubtreeGlobs">Optional glob patterns to filter browse subtrees.</param> /// <param name="browseSubtreeGlobs">Optional glob patterns to filter browse subtrees.</param>
/// <param name="offset">The zero-based offset into the result set.</param> /// <param name="offset">The zero-based offset into the result set.</param>
/// <param name="pageSize">The maximum number of results to return.</param> /// <param name="pageSize">The maximum number of results to return.</param>
/// <returns>The query result containing the requested page of matching objects.</returns>
public static GalaxyHierarchyQueryResult Project( public static GalaxyHierarchyQueryResult Project(
GalaxyHierarchyCacheEntry entry, GalaxyHierarchyCacheEntry entry,
DiscoverHierarchyRequest request, DiscoverHierarchyRequest request,
@@ -131,6 +133,7 @@ public static class GalaxyHierarchyProjector
/// <summary>Finds an object in the hierarchy by its tag address.</summary> /// <summary>Finds an object in the hierarchy by its tag address.</summary>
/// <param name="entry">The Galaxy hierarchy cache entry.</param> /// <param name="entry">The Galaxy hierarchy cache entry.</param>
/// <param name="tagAddress">The tag address to search for.</param> /// <param name="tagAddress">The tag address to search for.</param>
/// <returns>The matching Galaxy object, or <c>null</c> if not found.</returns>
public static GalaxyObject? FindObjectForTag( public static GalaxyObject? FindObjectForTag(
GalaxyHierarchyCacheEntry entry, GalaxyHierarchyCacheEntry entry,
string tagAddress) string tagAddress)
@@ -148,6 +151,7 @@ public static class GalaxyHierarchyProjector
/// <summary>Finds an attribute in the hierarchy by its tag address.</summary> /// <summary>Finds an attribute in the hierarchy by its tag address.</summary>
/// <param name="entry">The Galaxy hierarchy cache entry.</param> /// <param name="entry">The Galaxy hierarchy cache entry.</param>
/// <param name="tagAddress">The tag address to search for.</param> /// <param name="tagAddress">The tag address to search for.</param>
/// <returns>The matching Galaxy attribute, or <c>null</c> if not found.</returns>
public static GalaxyAttribute? FindAttributeForTag( public static GalaxyAttribute? FindAttributeForTag(
GalaxyHierarchyCacheEntry entry, GalaxyHierarchyCacheEntry entry,
string tagAddress) string tagAddress)
@@ -165,6 +169,7 @@ public static class GalaxyHierarchyProjector
/// <summary>Gets the contained path for an object by its gobject ID.</summary> /// <summary>Gets the contained path for an object by its gobject ID.</summary>
/// <param name="entry">The Galaxy hierarchy cache entry.</param> /// <param name="entry">The Galaxy hierarchy cache entry.</param>
/// <param name="gobjectId">The Galaxy object ID.</param> /// <param name="gobjectId">The Galaxy object ID.</param>
/// <returns>The contained path string, or an empty string if the object is not found.</returns>
public static string GetContainedPath( public static string GetContainedPath(
GalaxyHierarchyCacheEntry entry, GalaxyHierarchyCacheEntry entry,
int gobjectId) int gobjectId)
@@ -282,6 +287,7 @@ public static class GalaxyHierarchyProjector
/// <summary>Computes a stable filter signature for memoization purposes.</summary> /// <summary>Computes a stable filter signature for memoization purposes.</summary>
/// <param name="request">The discovery hierarchy request.</param> /// <param name="request">The discovery hierarchy request.</param>
/// <param name="browseSubtreeGlobs">Optional glob patterns to filter browse subtrees.</param> /// <param name="browseSubtreeGlobs">Optional glob patterns to filter browse subtrees.</param>
/// <returns>A string key that uniquely identifies the combination of filter parameters.</returns>
public static string ComputeFilterSignature( public static string ComputeFilterSignature(
DiscoverHierarchyRequest request, DiscoverHierarchyRequest request,
IReadOnlyList<string>? browseSubtreeGlobs) IReadOnlyList<string>? browseSubtreeGlobs)
@@ -15,8 +15,7 @@ namespace ZB.MOM.WW.MxGateway.Server.Galaxy;
/// </summary> /// </summary>
public sealed class GalaxyRepository(GalaxyRepositoryOptions options) : IGalaxyRepository public sealed class GalaxyRepository(GalaxyRepositoryOptions options) : IGalaxyRepository
{ {
/// <summary>Tests the connection to the Galaxy Repository database.</summary> /// <inheritdoc />
/// <param name="ct">Token to cancel the asynchronous operation.</param>
public async Task<bool> TestConnectionAsync(CancellationToken ct = default) public async Task<bool> TestConnectionAsync(CancellationToken ct = default)
{ {
try try
@@ -31,8 +30,7 @@ public sealed class GalaxyRepository(GalaxyRepositoryOptions options) : IGalaxyR
catch (InvalidOperationException) { return false; } catch (InvalidOperationException) { return false; }
} }
/// <summary>Retrieves the last deployment time from the Galaxy Repository.</summary> /// <inheritdoc />
/// <param name="ct">Token to cancel the asynchronous operation.</param>
public async Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default) public async Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default)
{ {
using SqlConnection conn = new(options.ConnectionString); using SqlConnection conn = new(options.ConnectionString);
@@ -43,8 +41,7 @@ public sealed class GalaxyRepository(GalaxyRepositoryOptions options) : IGalaxyR
return result is DateTime dt ? dt : null; return result is DateTime dt ? dt : null;
} }
/// <summary>Retrieves the complete hierarchy of Galaxy objects from the repository.</summary> /// <inheritdoc />
/// <param name="ct">Token to cancel the asynchronous operation.</param>
public async Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default) public async Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default)
{ {
List<GalaxyHierarchyRow> rows = new(); List<GalaxyHierarchyRow> rows = new();
@@ -81,8 +78,7 @@ public sealed class GalaxyRepository(GalaxyRepositoryOptions options) : IGalaxyR
return rows; return rows;
} }
/// <summary>Retrieves all attributes for Galaxy objects from the repository.</summary> /// <inheritdoc />
/// <param name="ct">Token to cancel the asynchronous operation.</param>
public async Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default) public async Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default)
{ {
List<GalaxyAttributeRow> rows = new(); List<GalaxyAttributeRow> rows = new();
@@ -13,6 +13,7 @@ public interface IGalaxyHierarchyCache
/// refresh. /// refresh.
/// </summary> /// </summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task RefreshAsync(CancellationToken cancellationToken); Task RefreshAsync(CancellationToken cancellationToken);
/// <summary> /// <summary>
@@ -21,5 +22,6 @@ public interface IGalaxyHierarchyCache
/// very first request after gateway start. /// very first request after gateway start.
/// </summary> /// </summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task WaitForFirstLoadAsync(CancellationToken cancellationToken); Task WaitForFirstLoadAsync(CancellationToken cancellationToken);
} }
@@ -13,6 +13,7 @@ public interface IGalaxyHierarchySnapshotStore
/// </summary> /// </summary>
/// <param name="snapshot">The browse dataset to persist.</param> /// <param name="snapshot">The browse dataset to persist.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous save operation.</returns>
Task SaveAsync(GalaxyHierarchySnapshot snapshot, CancellationToken cancellationToken); Task SaveAsync(GalaxyHierarchySnapshot snapshot, CancellationToken cancellationToken);
/// <summary> /// <summary>
@@ -14,17 +14,21 @@ public interface IGalaxyRepository
{ {
/// <summary>Tests the connection to the Galaxy Repository database.</summary> /// <summary>Tests the connection to the Galaxy Repository database.</summary>
/// <param name="ct">Token to cancel the asynchronous operation.</param> /// <param name="ct">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to <see langword="true"/> if the connection succeeds; otherwise <see langword="false"/>.</returns>
Task<bool> TestConnectionAsync(CancellationToken ct = default); Task<bool> TestConnectionAsync(CancellationToken ct = default);
/// <summary>Retrieves the last deployment time from the Galaxy Repository.</summary> /// <summary>Retrieves the last deployment time from the Galaxy Repository.</summary>
/// <param name="ct">Token to cancel the asynchronous operation.</param> /// <param name="ct">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the last deploy time, or <see langword="null"/> if not available.</returns>
Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default); Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default);
/// <summary>Retrieves the complete hierarchy of Galaxy objects from the repository.</summary> /// <summary>Retrieves the complete hierarchy of Galaxy objects from the repository.</summary>
/// <param name="ct">Token to cancel the asynchronous operation.</param> /// <param name="ct">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the list of hierarchy rows.</returns>
Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default); Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default);
/// <summary>Retrieves all attributes for Galaxy objects from the repository.</summary> /// <summary>Retrieves all attributes for Galaxy objects from the repository.</summary>
/// <param name="ct">Token to cancel the asynchronous operation.</param> /// <param name="ct">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the list of attribute rows.</returns>
Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default); Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default);
} }
@@ -18,12 +18,7 @@ public sealed class EventStreamService(
IDashboardEventBroadcaster dashboardEventBroadcaster, IDashboardEventBroadcaster dashboardEventBroadcaster,
ILogger<EventStreamService> logger) : IEventStreamService ILogger<EventStreamService> logger) : IEventStreamService
{ {
/// <summary> /// <inheritdoc />
/// Streams events from a session to the client asynchronously.
/// </summary>
/// <param name="request">Stream events request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Async enumerable of MX events.</returns>
public async IAsyncEnumerable<MxEvent> StreamEventsAsync( public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
StreamEventsRequest request, StreamEventsRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken) [EnumeratorCancellation] CancellationToken cancellationToken)
@@ -13,6 +13,7 @@ public static class GalaxyProtoMapper
/// <summary>Maps Galaxy hierarchy and attribute rows to Galaxy object protos.</summary> /// <summary>Maps Galaxy hierarchy and attribute rows to Galaxy object protos.</summary>
/// <param name="hierarchy">Hierarchy rows from Galaxy Repository.</param> /// <param name="hierarchy">Hierarchy rows from Galaxy Repository.</param>
/// <param name="attributes">Attribute rows from Galaxy Repository.</param> /// <param name="attributes">Attribute rows from Galaxy Repository.</param>
/// <returns>An enumerable of mapped Galaxy object protos.</returns>
public static IEnumerable<GalaxyObject> MapHierarchy( public static IEnumerable<GalaxyObject> MapHierarchy(
IReadOnlyList<GalaxyHierarchyRow> hierarchy, IReadOnlyList<GalaxyHierarchyRow> hierarchy,
IReadOnlyList<GalaxyAttributeRow> attributes) IReadOnlyList<GalaxyAttributeRow> attributes)
@@ -30,6 +31,7 @@ public static class GalaxyProtoMapper
/// <summary>Maps a Galaxy hierarchy row to a Galaxy object proto.</summary> /// <summary>Maps a Galaxy hierarchy row to a Galaxy object proto.</summary>
/// <param name="row">Hierarchy row from Galaxy Repository.</param> /// <param name="row">Hierarchy row from Galaxy Repository.</param>
/// <param name="attributesByGobjectId">Attributes indexed by gobject ID.</param> /// <param name="attributesByGobjectId">Attributes indexed by gobject ID.</param>
/// <returns>The mapped Galaxy object proto.</returns>
public static GalaxyObject MapObject( public static GalaxyObject MapObject(
GalaxyHierarchyRow row, GalaxyHierarchyRow row,
IReadOnlyDictionary<int, List<GalaxyAttributeRow>> attributesByGobjectId) IReadOnlyDictionary<int, List<GalaxyAttributeRow>> attributesByGobjectId)
@@ -60,6 +62,7 @@ public static class GalaxyProtoMapper
/// <summary>Maps a Galaxy attribute row to a Galaxy attribute proto.</summary> /// <summary>Maps a Galaxy attribute row to a Galaxy attribute proto.</summary>
/// <param name="row">Attribute row from Galaxy Repository.</param> /// <param name="row">Attribute row from Galaxy Repository.</param>
/// <returns>The mapped Galaxy attribute proto.</returns>
public static GalaxyAttribute MapAttribute(GalaxyAttributeRow row) => new() public static GalaxyAttribute MapAttribute(GalaxyAttributeRow row) => new()
{ {
AttributeName = row.AttributeName, AttributeName = row.AttributeName,
@@ -12,6 +12,7 @@ public interface IEventStreamService
/// </summary> /// </summary>
/// <param name="request">Request payload.</param> /// <param name="request">Request payload.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>An async enumerable of MXAccess events.</returns>
IAsyncEnumerable<MxEvent> StreamEventsAsync( IAsyncEnumerable<MxEvent> StreamEventsAsync(
StreamEventsRequest request, StreamEventsRequest request,
CancellationToken cancellationToken); CancellationToken cancellationToken);
@@ -162,15 +162,6 @@ public sealed class MxAccessGatewayService(
} }
/// <inheritdoc /> /// <inheritdoc />
/// <remarks>
/// Surfaces the public AcknowledgeAlarm RPC. Acknowledgement is
/// session-less: the gateway routes it through the always-on
/// <see cref="IGatewayAlarmService"/> monitor session. An
/// <c>alarm_full_reference</c> that parses as a canonical GUID forwards
/// to <c>AcknowledgeAlarmCommand</c>; a <c>Provider!Group.Tag</c>
/// reference forwards to <c>AcknowledgeAlarmByNameCommand</c>; anything
/// else returns an <c>InvalidRequest</c> diagnostic in the reply.
/// </remarks>
public override async Task<AcknowledgeAlarmReply> AcknowledgeAlarm( public override async Task<AcknowledgeAlarmReply> AcknowledgeAlarm(
AcknowledgeAlarmRequest request, AcknowledgeAlarmRequest request,
ServerCallContext context) ServerCallContext context)
@@ -193,14 +184,6 @@ public sealed class MxAccessGatewayService(
} }
/// <inheritdoc /> /// <inheritdoc />
/// <remarks>
/// Surfaces the public StreamAlarms RPC — the session-less central
/// alarm feed. The stream opens with one <c>active_alarm</c> per
/// currently-active alarm, then a single <c>snapshot_complete</c>, then
/// a <c>transition</c> for every subsequent change. Served by the
/// gateway's always-on <see cref="IGatewayAlarmService"/> monitor; any
/// number of clients fan out from the single monitor.
/// </remarks>
public override async Task StreamAlarms( public override async Task StreamAlarms(
StreamAlarmsRequest request, StreamAlarmsRequest request,
IServerStreamWriter<AlarmFeedMessage> responseStream, IServerStreamWriter<AlarmFeedMessage> responseStream,
@@ -224,12 +207,6 @@ public sealed class MxAccessGatewayService(
} }
/// <inheritdoc /> /// <inheritdoc />
/// <remarks>
/// Snapshot of the active-alarm cache maintained by the gateway's
/// always-on alarm monitor. Streams one <see cref="ActiveAlarmSnapshot"/>
/// per currently-active alarm and completes — no transitions are
/// emitted. Use <c>StreamAlarms</c> for a live transition feed.
/// </remarks>
public override async Task QueryActiveAlarms( public override async Task QueryActiveAlarms(
QueryActiveAlarmsRequest request, QueryActiveAlarmsRequest request,
IServerStreamWriter<ActiveAlarmSnapshot> responseStream, IServerStreamWriter<ActiveAlarmSnapshot> responseStream,
@@ -23,6 +23,7 @@ public sealed class MxAccessGrpcMapper
/// Maps a gRPC MX command request to a worker command. /// Maps a gRPC MX command request to a worker command.
/// </summary> /// </summary>
/// <param name="request">Request payload.</param> /// <param name="request">Request payload.</param>
/// <returns>The mapped worker command.</returns>
public WorkerCommand MapCommand(MxCommandRequest request) public WorkerCommand MapCommand(MxCommandRequest request)
{ {
ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(request);
@@ -39,6 +40,7 @@ public sealed class MxAccessGrpcMapper
/// Maps a worker command reply to a gRPC MX command reply. /// Maps a worker command reply to a gRPC MX command reply.
/// </summary> /// </summary>
/// <param name="reply">Worker command reply.</param> /// <param name="reply">Worker command reply.</param>
/// <returns>The mapped gRPC command reply.</returns>
public MxCommandReply MapCommandReply(WorkerCommandReply reply) public MxCommandReply MapCommandReply(WorkerCommandReply reply)
{ {
ArgumentNullException.ThrowIfNull(reply); ArgumentNullException.ThrowIfNull(reply);
@@ -58,6 +60,7 @@ public sealed class MxAccessGrpcMapper
/// Maps a worker event to a gRPC MX event. /// Maps a worker event to a gRPC MX event.
/// </summary> /// </summary>
/// <param name="workerEvent">Worker event to map.</param> /// <param name="workerEvent">Worker event to map.</param>
/// <returns>The mapped gRPC MX event.</returns>
public MxEvent MapEvent(WorkerEvent workerEvent) public MxEvent MapEvent(WorkerEvent workerEvent)
{ {
ArgumentNullException.ThrowIfNull(workerEvent); ArgumentNullException.ThrowIfNull(workerEvent);
@@ -73,6 +76,7 @@ public sealed class MxAccessGrpcMapper
/// Creates an OK protocol status. /// Creates an OK protocol status.
/// </summary> /// </summary>
/// <param name="message">Status message.</param> /// <param name="message">Status message.</param>
/// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.Ok"/>.</returns>
public static ProtocolStatus Ok(string message = "OK") public static ProtocolStatus Ok(string message = "OK")
{ {
return new ProtocolStatus return new ProtocolStatus
@@ -86,6 +90,7 @@ public sealed class MxAccessGrpcMapper
/// Creates an InvalidRequest protocol status. /// Creates an InvalidRequest protocol status.
/// </summary> /// </summary>
/// <param name="message">Status message.</param> /// <param name="message">Status message.</param>
/// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.InvalidRequest"/>.</returns>
public static ProtocolStatus InvalidRequest(string message) public static ProtocolStatus InvalidRequest(string message)
{ {
return new ProtocolStatus return new ProtocolStatus
@@ -99,6 +104,7 @@ public sealed class MxAccessGrpcMapper
/// Creates a SessionNotFound protocol status. /// Creates a SessionNotFound protocol status.
/// </summary> /// </summary>
/// <param name="message">Status message.</param> /// <param name="message">Status message.</param>
/// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.SessionNotFound"/>.</returns>
public static ProtocolStatus SessionNotFound(string message) public static ProtocolStatus SessionNotFound(string message)
{ {
return new ProtocolStatus return new ProtocolStatus
@@ -112,6 +118,7 @@ public sealed class MxAccessGrpcMapper
/// Creates a SessionNotReady protocol status. /// Creates a SessionNotReady protocol status.
/// </summary> /// </summary>
/// <param name="message">Status message.</param> /// <param name="message">Status message.</param>
/// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.SessionNotReady"/>.</returns>
public static ProtocolStatus SessionNotReady(string message) public static ProtocolStatus SessionNotReady(string message)
{ {
return new ProtocolStatus return new ProtocolStatus
@@ -125,6 +132,7 @@ public sealed class MxAccessGrpcMapper
/// Creates a WorkerUnavailable protocol status. /// Creates a WorkerUnavailable protocol status.
/// </summary> /// </summary>
/// <param name="message">Status message.</param> /// <param name="message">Status message.</param>
/// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.WorkerUnavailable"/>.</returns>
public static ProtocolStatus WorkerUnavailable(string message) public static ProtocolStatus WorkerUnavailable(string message)
{ {
return new ProtocolStatus return new ProtocolStatus
@@ -138,6 +146,7 @@ public sealed class MxAccessGrpcMapper
/// Creates a Timeout protocol status. /// Creates a Timeout protocol status.
/// </summary> /// </summary>
/// <param name="message">Status message.</param> /// <param name="message">Status message.</param>
/// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.Timeout"/>.</returns>
public static ProtocolStatus Timeout(string message) public static ProtocolStatus Timeout(string message)
{ {
return new ProtocolStatus return new ProtocolStatus
@@ -151,6 +160,7 @@ public sealed class MxAccessGrpcMapper
/// Creates a Canceled protocol status. /// Creates a Canceled protocol status.
/// </summary> /// </summary>
/// <param name="message">Status message.</param> /// <param name="message">Status message.</param>
/// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.Canceled"/>.</returns>
public static ProtocolStatus Canceled(string message) public static ProtocolStatus Canceled(string message)
{ {
return new ProtocolStatus return new ProtocolStatus
@@ -164,6 +174,7 @@ public sealed class MxAccessGrpcMapper
/// Creates a ProtocolViolation protocol status. /// Creates a ProtocolViolation protocol status.
/// </summary> /// </summary>
/// <param name="message">Status message.</param> /// <param name="message">Status message.</param>
/// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.ProtocolViolation"/>.</returns>
public static ProtocolStatus ProtocolViolation(string message) public static ProtocolStatus ProtocolViolation(string message)
{ {
return new ProtocolStatus return new ProtocolStatus
@@ -380,6 +380,7 @@ public sealed class GatewayMetrics : IDisposable
/// <summary> /// <summary>
/// Returns a snapshot of all current metric values. /// Returns a snapshot of all current metric values.
/// </summary> /// </summary>
/// <returns>A consistent snapshot of the current metric counters and gauges.</returns>
public GatewayMetricsSnapshot GetSnapshot() public GatewayMetricsSnapshot GetSnapshot()
{ {
lock (_syncRoot) lock (_syncRoot)
@@ -19,7 +19,10 @@ public sealed class CanonicalAuditWriter(
SqliteCanonicalAuditStore store, SqliteCanonicalAuditStore store,
ILogger<CanonicalAuditWriter> logger) : IAuditWriter ILogger<CanonicalAuditWriter> logger) : IAuditWriter
{ {
/// <inheritdoc /> /// <summary>Persists the audit event to the canonical store; swallows and logs any write failure rather than propagating it.</summary>
/// <param name="auditEvent">The audit event to persist.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task WriteAsync(AuditEvent auditEvent, CancellationToken cancellationToken = default) public async Task WriteAsync(AuditEvent auditEvent, CancellationToken cancellationToken = default)
{ {
ArgumentNullException.ThrowIfNull(auditEvent); ArgumentNullException.ThrowIfNull(auditEvent);
@@ -43,7 +43,10 @@ public sealed class CanonicalForwardingApiKeyAuditStore(
/// <summary>The library's keyless schema-init event type.</summary> /// <summary>The library's keyless schema-init event type.</summary>
private const string InitDbEventType = "init-db"; private const string InitDbEventType = "init-db";
/// <inheritdoc /> /// <summary>Converts the library audit entry to a canonical <see cref="AuditEvent"/> and forwards it through the gateway's audit writer.</summary>
/// <param name="entry">The API key audit entry to append.</param>
/// <param name="ct">Token to observe for cancellation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task AppendAsync(ApiKeyAuditEntry entry, CancellationToken ct) public async Task AppendAsync(ApiKeyAuditEntry entry, CancellationToken ct)
{ {
ArgumentNullException.ThrowIfNull(entry); ArgumentNullException.ThrowIfNull(entry);
@@ -71,7 +74,10 @@ public sealed class CanonicalForwardingApiKeyAuditStore(
await auditWriter.WriteAsync(auditEvent, ct).ConfigureAwait(false); await auditWriter.WriteAsync(auditEvent, ct).ConfigureAwait(false);
} }
/// <inheritdoc /> /// <summary>Reads recent audit events from the canonical store and maps them back to library-compatible <see cref="ApiKeyAuditEntry"/> records.</summary>
/// <param name="limit">Maximum number of entries to return.</param>
/// <param name="ct">Token to observe for cancellation.</param>
/// <returns>A task that resolves to the most recent audit entries, up to <paramref name="limit"/> items.</returns>
public async Task<IReadOnlyList<ApiKeyAuditEntry>> ListRecentAsync(int limit, CancellationToken ct) public async Task<IReadOnlyList<ApiKeyAuditEntry>> ListRecentAsync(int limit, CancellationToken ct)
{ {
IReadOnlyList<AuditEvent> events = await store.ListRecentAsync(limit, ct).ConfigureAwait(false); IReadOnlyList<AuditEvent> events = await store.ListRecentAsync(limit, ct).ConfigureAwait(false);
@@ -43,6 +43,7 @@ public sealed class SqliteCanonicalAuditStore(AuthSqliteConnectionFactory connec
/// <summary>Inserts a canonical audit event into the <c>audit_event</c> table.</summary> /// <summary>Inserts a canonical audit event into the <c>audit_event</c> table.</summary>
/// <param name="auditEvent">The canonical event to persist.</param> /// <param name="auditEvent">The canonical event to persist.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param> /// <param name="cancellationToken">Token to observe for cancellation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task InsertAsync(AuditEvent auditEvent, CancellationToken cancellationToken) public async Task InsertAsync(AuditEvent auditEvent, CancellationToken cancellationToken)
{ {
ArgumentNullException.ThrowIfNull(auditEvent); ArgumentNullException.ThrowIfNull(auditEvent);
@@ -79,6 +80,7 @@ public sealed class SqliteCanonicalAuditStore(AuthSqliteConnectionFactory connec
/// <summary>Returns the most recent canonical audit events, newest first.</summary> /// <summary>Returns the most recent canonical audit events, newest first.</summary>
/// <param name="limit">Maximum number of events to return.</param> /// <param name="limit">Maximum number of events to return.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param> /// <param name="cancellationToken">Token to observe for cancellation.</param>
/// <returns>A task that resolves to the most recent audit events, up to <paramref name="limit"/>.</returns>
public async Task<IReadOnlyList<AuditEvent>> ListRecentAsync(int limit, CancellationToken cancellationToken) public async Task<IReadOnlyList<AuditEvent>> ListRecentAsync(int limit, CancellationToken cancellationToken)
{ {
if (limit <= 0) if (limit <= 0)
@@ -26,6 +26,7 @@ public sealed class ApiKeyAdminCliRunner(ApiKeyAdminCommands commands)
/// <param name="command">API key administration command to execute.</param> /// <param name="command">API key administration command to execute.</param>
/// <param name="output">Text writer for command output.</param> /// <param name="output">Text writer for command output.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the exit code (always 0 on success).</returns>
public async Task<int> RunAsync( public async Task<int> RunAsync(
ApiKeyAdminCommand command, ApiKeyAdminCommand command,
TextWriter output, TextWriter output,
@@ -6,6 +6,7 @@ public sealed record ApiKeyAdminParseResult(
string? Error) string? Error)
{ {
/// <summary>Returns a result indicating the input was not an API key command.</summary> /// <summary>Returns a result indicating the input was not an API key command.</summary>
/// <returns>A parse result with <see cref="IsApiKeyCommand"/> set to false.</returns>
public static ApiKeyAdminParseResult NotApiKeyCommand() public static ApiKeyAdminParseResult NotApiKeyCommand()
{ {
return new ApiKeyAdminParseResult(false, null, null); return new ApiKeyAdminParseResult(false, null, null);
@@ -13,6 +14,7 @@ public sealed record ApiKeyAdminParseResult(
/// <summary>Returns a successful parse result with the parsed API key command.</summary> /// <summary>Returns a successful parse result with the parsed API key command.</summary>
/// <param name="command">Parsed API key administration command.</param> /// <param name="command">Parsed API key administration command.</param>
/// <returns>A parse result with <see cref="IsApiKeyCommand"/> set to true and the command populated.</returns>
public static ApiKeyAdminParseResult Success(ApiKeyAdminCommand command) public static ApiKeyAdminParseResult Success(ApiKeyAdminCommand command)
{ {
return new ApiKeyAdminParseResult(true, command, null); return new ApiKeyAdminParseResult(true, command, null);
@@ -20,6 +22,7 @@ public sealed record ApiKeyAdminParseResult(
/// <summary>Returns a parse result with the specified error message.</summary> /// <summary>Returns a parse result with the specified error message.</summary>
/// <param name="error">Error message describing the parse failure.</param> /// <param name="error">Error message describing the parse failure.</param>
/// <returns>A parse result with <see cref="IsApiKeyCommand"/> set to true and the error message populated.</returns>
public static ApiKeyAdminParseResult Fail(string error) public static ApiKeyAdminParseResult Fail(string error)
{ {
return new ApiKeyAdminParseResult(true, null, error); return new ApiKeyAdminParseResult(true, null, error);
@@ -12,6 +12,7 @@ public static class ApiKeyConstraintSerializer
/// <summary>Serializes API key constraints to JSON, or returns null if the constraints are empty.</summary> /// <summary>Serializes API key constraints to JSON, or returns null if the constraints are empty.</summary>
/// <param name="constraints">The constraints to serialize.</param> /// <param name="constraints">The constraints to serialize.</param>
/// <returns>A JSON string representing the constraints, or <see langword="null"/> if empty.</returns>
public static string? Serialize(ApiKeyConstraints constraints) public static string? Serialize(ApiKeyConstraints constraints)
{ {
ArgumentNullException.ThrowIfNull(constraints); ArgumentNullException.ThrowIfNull(constraints);
@@ -20,6 +21,7 @@ public static class ApiKeyConstraintSerializer
/// <summary>Deserializes API key constraints from JSON, or returns empty constraints if JSON is null or whitespace.</summary> /// <summary>Deserializes API key constraints from JSON, or returns empty constraints if JSON is null or whitespace.</summary>
/// <param name="json">The JSON string to deserialize.</param> /// <param name="json">The JSON string to deserialize.</param>
/// <returns>The deserialized <see cref="ApiKeyConstraints"/>, or <see cref="ApiKeyConstraints.Empty"/> when <paramref name="json"/> is null or whitespace.</returns>
public static ApiKeyConstraints Deserialize(string? json) public static ApiKeyConstraints Deserialize(string? json)
{ {
if (string.IsNullOrWhiteSpace(json)) if (string.IsNullOrWhiteSpace(json))
@@ -16,10 +16,7 @@ public sealed class ConstraintEnforcer(
IGalaxyHierarchyCache cache, IGalaxyHierarchyCache cache,
IAuditWriter auditWriter) : IConstraintEnforcer IAuditWriter auditWriter) : IConstraintEnforcer
{ {
/// <summary>Checks read constraints on a tag address.</summary> /// <inheritdoc />
/// <param name="identity">The API key identity to check constraints for.</param>
/// <param name="tagAddress">Tag address to validate.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param>
public Task<ConstraintFailure?> CheckReadTagAsync( public Task<ConstraintFailure?> CheckReadTagAsync(
ApiKeyIdentity? identity, ApiKeyIdentity? identity,
string tagAddress, string tagAddress,
@@ -34,12 +31,7 @@ public sealed class ConstraintEnforcer(
return Task.FromResult(CheckReadTarget(constraints, tagAddress)); return Task.FromResult(CheckReadTarget(constraints, tagAddress));
} }
/// <summary>Checks read constraints on a server and item handle.</summary> /// <inheritdoc />
/// <param name="identity">The API key identity to check constraints for.</param>
/// <param name="session">The gateway session containing handle registrations.</param>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param>
public Task<ConstraintFailure?> CheckReadHandleAsync( public Task<ConstraintFailure?> CheckReadHandleAsync(
ApiKeyIdentity? identity, ApiKeyIdentity? identity,
GatewaySession session, GatewaySession session,
@@ -61,12 +53,7 @@ public sealed class ConstraintEnforcer(
return Task.FromResult(CheckReadTarget(constraints, registration.TagAddress)); return Task.FromResult(CheckReadTarget(constraints, registration.TagAddress));
} }
/// <summary>Checks write constraints on a server and item handle.</summary> /// <inheritdoc />
/// <param name="identity">The API key identity to check constraints for.</param>
/// <param name="session">The gateway session containing handle registrations.</param>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param>
public Task<ConstraintFailure?> CheckWriteHandleAsync( public Task<ConstraintFailure?> CheckWriteHandleAsync(
ApiKeyIdentity? identity, ApiKeyIdentity? identity,
GatewaySession session, GatewaySession session,
@@ -115,12 +102,7 @@ public sealed class ConstraintEnforcer(
return Task.FromResult<ConstraintFailure?>(null); return Task.FromResult<ConstraintFailure?>(null);
} }
/// <summary>Records a constraint denial audit entry.</summary> /// <inheritdoc />
/// <param name="identity">The API key identity that was denied.</param>
/// <param name="commandKind">The command type (e.g., read, write).</param>
/// <param name="target">The target being accessed (tag address or handle).</param>
/// <param name="failure">The constraint failure details.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param>
public async Task RecordDenialAsync( public async Task RecordDenialAsync(
ApiKeyIdentity? identity, ApiKeyIdentity? identity,
string commandKind, string commandKind,
@@ -6,12 +6,10 @@ public sealed class GatewayRequestIdentityAccessor : IGatewayRequestIdentityAcce
{ {
private readonly AsyncLocal<ApiKeyIdentity?> currentIdentity = new(); private readonly AsyncLocal<ApiKeyIdentity?> currentIdentity = new();
/// <summary>Gets the current request identity.</summary> /// <inheritdoc />
public ApiKeyIdentity? Current => currentIdentity.Value; public ApiKeyIdentity? Current => currentIdentity.Value;
/// <summary>Sets the current identity and returns a scope that restores the previous identity.</summary> /// <inheritdoc />
/// <param name="identity">The identity to push.</param>
/// <returns>Disposable scope.</returns>
public IDisposable Push(ApiKeyIdentity identity) public IDisposable Push(ApiKeyIdentity identity)
{ {
ArgumentNullException.ThrowIfNull(identity); ArgumentNullException.ThrowIfNull(identity);
@@ -13,6 +13,7 @@ public static class GrpcAuthorizationServiceCollectionExtensions
/// Registers gRPC authorization middleware and scope resolver. /// Registers gRPC authorization middleware and scope resolver.
/// </summary> /// </summary>
/// <param name="services">Service collection to register dependencies into.</param> /// <param name="services">Service collection to register dependencies into.</param>
/// <returns>The same <paramref name="services"/> for fluent chaining.</returns>
public static IServiceCollection AddGatewayGrpcAuthorization(this IServiceCollection services) public static IServiceCollection AddGatewayGrpcAuthorization(this IServiceCollection services)
{ {
services.AddSingleton<GatewayGrpcScopeResolver>(); services.AddSingleton<GatewayGrpcScopeResolver>();
@@ -9,6 +9,7 @@ public interface IConstraintEnforcer
/// <param name="identity">The API key identity.</param> /// <param name="identity">The API key identity.</param>
/// <param name="tagAddress">Tag address to check.</param> /// <param name="tagAddress">Tag address to check.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param> /// <param name="cancellationToken">Token to observe for cancellation.</param>
/// <returns>A task that resolves to the constraint failure, or null if the check passes.</returns>
Task<ConstraintFailure?> CheckReadTagAsync( Task<ConstraintFailure?> CheckReadTagAsync(
ApiKeyIdentity? identity, ApiKeyIdentity? identity,
string tagAddress, string tagAddress,
@@ -20,6 +21,7 @@ public interface IConstraintEnforcer
/// <param name="serverHandle">The MXAccess server handle.</param> /// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param> /// <param name="itemHandle">The MXAccess item handle.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param> /// <param name="cancellationToken">Token to observe for cancellation.</param>
/// <returns>A task that resolves to the constraint failure, or null if the check passes.</returns>
Task<ConstraintFailure?> CheckReadHandleAsync( Task<ConstraintFailure?> CheckReadHandleAsync(
ApiKeyIdentity? identity, ApiKeyIdentity? identity,
GatewaySession session, GatewaySession session,
@@ -33,6 +35,7 @@ public interface IConstraintEnforcer
/// <param name="serverHandle">The MXAccess server handle.</param> /// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param> /// <param name="itemHandle">The MXAccess item handle.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param> /// <param name="cancellationToken">Token to observe for cancellation.</param>
/// <returns>A task that resolves to the constraint failure, or null if the check passes.</returns>
Task<ConstraintFailure?> CheckWriteHandleAsync( Task<ConstraintFailure?> CheckWriteHandleAsync(
ApiKeyIdentity? identity, ApiKeyIdentity? identity,
GatewaySession session, GatewaySession session,
@@ -46,6 +49,7 @@ public interface IConstraintEnforcer
/// <param name="target">The target of the denied command.</param> /// <param name="target">The target of the denied command.</param>
/// <param name="failure">The constraint failure details.</param> /// <param name="failure">The constraint failure details.</param>
/// <param name="cancellationToken">Token to observe for cancellation.</param> /// <param name="cancellationToken">Token to observe for cancellation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task RecordDenialAsync( Task RecordDenialAsync(
ApiKeyIdentity? identity, ApiKeyIdentity? identity,
string commandKind, string commandKind,
@@ -10,5 +10,6 @@ public interface IGatewayRequestIdentityAccessor
/// <summary>Temporarily pushes an identity onto the scope stack, returning a handle to restore the previous state.</summary> /// <summary>Temporarily pushes an identity onto the scope stack, returning a handle to restore the previous state.</summary>
/// <param name="identity">API key identity to push.</param> /// <param name="identity">API key identity to push.</param>
/// <returns>A disposable scope that restores the previous identity when disposed.</returns>
IDisposable Push(ApiKeyIdentity identity); IDisposable Push(ApiKeyIdentity identity);
} }
@@ -16,6 +16,8 @@ public static class KestrelTlsInspector
/// <c>Certificate:Thumbprint</c>), meaning the gateway must supply a /// <c>Certificate:Thumbprint</c>), meaning the gateway must supply a
/// generated fallback certificate. /// generated fallback certificate.
/// </summary> /// </summary>
/// <param name="configuration">Application configuration containing the Kestrel endpoint settings.</param>
/// <returns><see langword="true"/> if a generated certificate is required; otherwise <see langword="false"/>.</returns>
public static bool RequiresGeneratedCertificate(IConfiguration configuration) public static bool RequiresGeneratedCertificate(IConfiguration configuration)
{ {
// A Kestrel default certificate applies to every endpoint that lacks its own. // A Kestrel default certificate applies to every endpoint that lacks its own.
@@ -20,6 +20,10 @@ public sealed class SelfSignedCertificateProvider
private readonly ILogger<SelfSignedCertificateProvider> _logger; private readonly ILogger<SelfSignedCertificateProvider> _logger;
private readonly TimeProvider _timeProvider; private readonly TimeProvider _timeProvider;
/// <summary>Initializes a new instance of the <see cref="SelfSignedCertificateProvider"/> class.</summary>
/// <param name="options">TLS configuration options controlling certificate subject, validity, and storage.</param>
/// <param name="logger">Logger for certificate lifecycle events.</param>
/// <param name="timeProvider">Time provider used to compute certificate validity windows.</param>
public SelfSignedCertificateProvider( public SelfSignedCertificateProvider(
TlsOptions options, TlsOptions options,
ILogger<SelfSignedCertificateProvider> logger, ILogger<SelfSignedCertificateProvider> logger,
@@ -31,6 +35,7 @@ public sealed class SelfSignedCertificateProvider
} }
/// <summary>Creates a fresh in-memory ECDSA P-256 self-signed certificate.</summary> /// <summary>Creates a fresh in-memory ECDSA P-256 self-signed certificate.</summary>
/// <returns>A new self-signed <see cref="X509Certificate2"/> with the configured SANs and validity period.</returns>
public X509Certificate2 GenerateCertificate() public X509Certificate2 GenerateCertificate()
{ {
using ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP256); using ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
@@ -89,6 +94,7 @@ public sealed class SelfSignedCertificateProvider
/// <summary>Loads the persisted certificate, regenerating when missing, /// <summary>Loads the persisted certificate, regenerating when missing,
/// expired (and allowed), or unreadable.</summary> /// expired (and allowed), or unreadable.</summary>
/// <returns>The loaded or newly generated <see cref="X509Certificate2"/>.</returns>
public X509Certificate2 LoadOrCreate() public X509Certificate2 LoadOrCreate()
{ {
string path = _options.SelfSignedCertPath; string path = _options.SelfSignedCertPath;
@@ -370,6 +370,7 @@ public sealed class GatewaySession
/// Determines whether the session lease has expired. /// Determines whether the session lease has expired.
/// </summary> /// </summary>
/// <param name="now">Current timestamp for comparison.</param> /// <param name="now">Current timestamp for comparison.</param>
/// <returns><c>true</c> if the lease has expired; otherwise <c>false</c>.</returns>
public bool IsLeaseExpired(DateTimeOffset now) public bool IsLeaseExpired(DateTimeOffset now)
{ {
lock (_syncRoot) lock (_syncRoot)
@@ -384,6 +385,7 @@ public sealed class GatewaySession
/// Attaches an event subscriber and returns a disposable lease. /// Attaches an event subscriber and returns a disposable lease.
/// </summary> /// </summary>
/// <param name="allowMultipleSubscribers">If true, allows multiple concurrent event subscribers.</param> /// <param name="allowMultipleSubscribers">If true, allows multiple concurrent event subscribers.</param>
/// <returns>A disposable that releases the subscriber slot when disposed.</returns>
public IDisposable AttachEventSubscriber(bool allowMultipleSubscribers) public IDisposable AttachEventSubscriber(bool allowMultipleSubscribers)
{ {
lock (_syncRoot) lock (_syncRoot)
@@ -412,6 +414,7 @@ public sealed class GatewaySession
/// </summary> /// </summary>
/// <param name="command">Worker command to invoke.</param> /// <param name="command">Worker command to invoke.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the worker command reply.</returns>
public async Task<WorkerCommandReply> InvokeAsync( public async Task<WorkerCommandReply> InvokeAsync(
WorkerCommand command, WorkerCommand command,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@@ -426,6 +429,7 @@ public sealed class GatewaySession
/// <param name="serverHandle">The MXAccess server handle.</param> /// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param> /// <param name="itemHandle">The MXAccess item handle.</param>
/// <param name="registration">The item registration if found.</param> /// <param name="registration">The item registration if found.</param>
/// <returns><c>true</c> if the item registration was found; otherwise <c>false</c>.</returns>
public bool TryGetItemRegistration( public bool TryGetItemRegistration(
int serverHandle, int serverHandle,
int itemHandle, int itemHandle,
@@ -487,6 +491,7 @@ public sealed class GatewaySession
/// <param name="serverHandle">Server handle returned by the worker.</param> /// <param name="serverHandle">Server handle returned by the worker.</param>
/// <param name="tagAddresses">Tag addresses to add.</param> /// <param name="tagAddresses">Tag addresses to add.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the per-item subscribe results.</returns>
public Task<IReadOnlyList<SubscribeResult>> AddItemBulkAsync( public Task<IReadOnlyList<SubscribeResult>> AddItemBulkAsync(
int serverHandle, int serverHandle,
IReadOnlyList<string> tagAddresses, IReadOnlyList<string> tagAddresses,
@@ -512,6 +517,7 @@ public sealed class GatewaySession
/// <param name="serverHandle">Server handle returned by the worker.</param> /// <param name="serverHandle">Server handle returned by the worker.</param>
/// <param name="itemHandles">Item handles to advise.</param> /// <param name="itemHandles">Item handles to advise.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the per-item subscribe results.</returns>
public Task<IReadOnlyList<SubscribeResult>> AdviseItemBulkAsync( public Task<IReadOnlyList<SubscribeResult>> AdviseItemBulkAsync(
int serverHandle, int serverHandle,
IReadOnlyList<int> itemHandles, IReadOnlyList<int> itemHandles,
@@ -537,6 +543,7 @@ public sealed class GatewaySession
/// <param name="serverHandle">Server handle returned by the worker.</param> /// <param name="serverHandle">Server handle returned by the worker.</param>
/// <param name="itemHandles">Item handles to remove.</param> /// <param name="itemHandles">Item handles to remove.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the per-item subscribe results.</returns>
public Task<IReadOnlyList<SubscribeResult>> RemoveItemBulkAsync( public Task<IReadOnlyList<SubscribeResult>> RemoveItemBulkAsync(
int serverHandle, int serverHandle,
IReadOnlyList<int> itemHandles, IReadOnlyList<int> itemHandles,
@@ -562,6 +569,7 @@ public sealed class GatewaySession
/// <param name="serverHandle">Server handle returned by the worker.</param> /// <param name="serverHandle">Server handle returned by the worker.</param>
/// <param name="itemHandles">Item handles to un-advise.</param> /// <param name="itemHandles">Item handles to un-advise.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the per-item subscribe results.</returns>
public Task<IReadOnlyList<SubscribeResult>> UnAdviseItemBulkAsync( public Task<IReadOnlyList<SubscribeResult>> UnAdviseItemBulkAsync(
int serverHandle, int serverHandle,
IReadOnlyList<int> itemHandles, IReadOnlyList<int> itemHandles,
@@ -587,6 +595,7 @@ public sealed class GatewaySession
/// <param name="serverHandle">Server handle returned by the worker.</param> /// <param name="serverHandle">Server handle returned by the worker.</param>
/// <param name="tagAddresses">Tag addresses to subscribe to.</param> /// <param name="tagAddresses">Tag addresses to subscribe to.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the per-item subscribe results.</returns>
public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync( public Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
int serverHandle, int serverHandle,
IReadOnlyList<string> tagAddresses, IReadOnlyList<string> tagAddresses,
@@ -612,6 +621,7 @@ public sealed class GatewaySession
/// <param name="serverHandle">Server handle returned by the worker.</param> /// <param name="serverHandle">Server handle returned by the worker.</param>
/// <param name="itemHandles">Item handles to unsubscribe from.</param> /// <param name="itemHandles">Item handles to unsubscribe from.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the per-item subscribe results.</returns>
public Task<IReadOnlyList<SubscribeResult>> UnsubscribeBulkAsync( public Task<IReadOnlyList<SubscribeResult>> UnsubscribeBulkAsync(
int serverHandle, int serverHandle,
IReadOnlyList<int> itemHandles, IReadOnlyList<int> itemHandles,
@@ -635,6 +645,7 @@ public sealed class GatewaySession
/// <param name="serverHandle">Server handle returned by the worker.</param> /// <param name="serverHandle">Server handle returned by the worker.</param>
/// <param name="entries">Write entries to execute.</param> /// <param name="entries">Write entries to execute.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the per-item write results.</returns>
public Task<IReadOnlyList<BulkWriteResult>> WriteBulkAsync( public Task<IReadOnlyList<BulkWriteResult>> WriteBulkAsync(
int serverHandle, int serverHandle,
IReadOnlyList<WriteBulkEntry> entries, IReadOnlyList<WriteBulkEntry> entries,
@@ -658,6 +669,7 @@ public sealed class GatewaySession
/// <param name="serverHandle">Server handle returned by the worker.</param> /// <param name="serverHandle">Server handle returned by the worker.</param>
/// <param name="entries">Write entries to execute.</param> /// <param name="entries">Write entries to execute.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the per-item write results.</returns>
public Task<IReadOnlyList<BulkWriteResult>> Write2BulkAsync( public Task<IReadOnlyList<BulkWriteResult>> Write2BulkAsync(
int serverHandle, int serverHandle,
IReadOnlyList<Write2BulkEntry> entries, IReadOnlyList<Write2BulkEntry> entries,
@@ -681,6 +693,7 @@ public sealed class GatewaySession
/// <param name="serverHandle">Server handle returned by the worker.</param> /// <param name="serverHandle">Server handle returned by the worker.</param>
/// <param name="entries">Write entries to execute.</param> /// <param name="entries">Write entries to execute.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the per-item write results.</returns>
public Task<IReadOnlyList<BulkWriteResult>> WriteSecuredBulkAsync( public Task<IReadOnlyList<BulkWriteResult>> WriteSecuredBulkAsync(
int serverHandle, int serverHandle,
IReadOnlyList<WriteSecuredBulkEntry> entries, IReadOnlyList<WriteSecuredBulkEntry> entries,
@@ -704,6 +717,7 @@ public sealed class GatewaySession
/// <param name="serverHandle">Server handle returned by the worker.</param> /// <param name="serverHandle">Server handle returned by the worker.</param>
/// <param name="entries">Write entries to execute.</param> /// <param name="entries">Write entries to execute.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the per-item write results.</returns>
public Task<IReadOnlyList<BulkWriteResult>> WriteSecured2BulkAsync( public Task<IReadOnlyList<BulkWriteResult>> WriteSecured2BulkAsync(
int serverHandle, int serverHandle,
IReadOnlyList<WriteSecured2BulkEntry> entries, IReadOnlyList<WriteSecured2BulkEntry> entries,
@@ -731,6 +745,7 @@ public sealed class GatewaySession
/// <param name="tagAddresses">Tag addresses to read.</param> /// <param name="tagAddresses">Tag addresses to read.</param>
/// <param name="timeout">Timeout for the read operation.</param> /// <param name="timeout">Timeout for the read operation.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the per-item read results.</returns>
public Task<IReadOnlyList<BulkReadResult>> ReadBulkAsync( public Task<IReadOnlyList<BulkReadResult>> ReadBulkAsync(
int serverHandle, int serverHandle,
IReadOnlyList<string> tagAddresses, IReadOnlyList<string> tagAddresses,
@@ -759,6 +774,7 @@ public sealed class GatewaySession
/// Reads events from the worker as an asynchronous enumerable stream. /// Reads events from the worker as an asynchronous enumerable stream.
/// </summary> /// </summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>An async enumerable of worker events.</returns>
public IAsyncEnumerable<WorkerEvent> ReadEventsAsync(CancellationToken cancellationToken) public IAsyncEnumerable<WorkerEvent> ReadEventsAsync(CancellationToken cancellationToken)
{ {
IWorkerClient workerClient = GetReadyWorkerClient(); IWorkerClient workerClient = GetReadyWorkerClient();
@@ -772,6 +788,7 @@ public sealed class GatewaySession
/// </summary> /// </summary>
/// <param name="reason">Reason for closing the session.</param> /// <param name="reason">Reason for closing the session.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the session close result.</returns>
/// <remarks> /// <remarks>
/// Concurrent close attempts are serialized by <c>_closeLock</c> so only one close /// Concurrent close attempts are serialized by <c>_closeLock</c> so only one close
/// runs at a time, but every read/write of <c>_state</c> still passes through /// runs at a time, but every read/write of <c>_state</c> still passes through
@@ -919,6 +936,7 @@ public sealed class GatewaySession
/// <summary> /// <summary>
/// Disposes the session and frees associated resources. /// Disposes the session and frees associated resources.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
/// <remarks> /// <remarks>
/// Acquires <c>_closeLock</c> once before disposing so an in-flight /// Acquires <c>_closeLock</c> once before disposing so an in-flight
/// <see cref="CloseAsync"/> finishes before the semaphore is released and /// <see cref="CloseAsync"/> finishes before the semaphore is released and
@@ -72,5 +72,6 @@ public interface ISessionManager
/// <summary>Shuts down all sessions and the session manager.</summary> /// <summary>Shuts down all sessions and the session manager.</summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task ShutdownAsync(CancellationToken cancellationToken); Task ShutdownAsync(CancellationToken cancellationToken);
} }
@@ -53,13 +53,7 @@ public sealed class SessionManager : ISessionManager
_sessionSlots = new SemaphoreSlim(_options.Sessions.MaxSessions, _options.Sessions.MaxSessions); _sessionSlots = new SemaphoreSlim(_options.Sessions.MaxSessions, _options.Sessions.MaxSessions);
} }
/// <summary> /// <inheritdoc />
/// Opens a new gateway session and connects to the worker.
/// </summary>
/// <param name="request">Session open request.</param>
/// <param name="clientIdentity">Client authentication identity.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Opened gateway session.</returns>
public async Task<GatewaySession> OpenSessionAsync( public async Task<GatewaySession> OpenSessionAsync(
SessionOpenRequest request, SessionOpenRequest request,
string? clientIdentity, string? clientIdentity,
@@ -123,12 +117,7 @@ public sealed class SessionManager : ISessionManager
} }
} }
/// <summary> /// <inheritdoc />
/// Attempts to retrieve a session by ID.
/// </summary>
/// <param name="sessionId">Session identifier.</param>
/// <param name="session">The session if found.</param>
/// <returns>True if session found; otherwise false.</returns>
public bool TryGetSession( public bool TryGetSession(
string sessionId, string sessionId,
[MaybeNullWhen(false)] out GatewaySession session) [MaybeNullWhen(false)] out GatewaySession session)
@@ -136,13 +125,7 @@ public sealed class SessionManager : ISessionManager
return _registry.TryGet(sessionId, out session); return _registry.TryGet(sessionId, out session);
} }
/// <summary> /// <inheritdoc />
/// Invokes a worker command on a session asynchronously.
/// </summary>
/// <param name="sessionId">Session identifier.</param>
/// <param name="command">Worker command.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Command reply.</returns>
public async Task<WorkerCommandReply> InvokeAsync( public async Task<WorkerCommandReply> InvokeAsync(
string sessionId, string sessionId,
WorkerCommand command, WorkerCommand command,
@@ -169,12 +152,7 @@ public sealed class SessionManager : ISessionManager
} }
} }
/// <summary> /// <inheritdoc />
/// Reads events from a session's event stream asynchronously.
/// </summary>
/// <param name="sessionId">Session identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Async enumerable of worker events.</returns>
public IAsyncEnumerable<WorkerEvent> ReadEventsAsync( public IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
string sessionId, string sessionId,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@@ -184,12 +162,7 @@ public sealed class SessionManager : ISessionManager
return session.ReadEventsAsync(cancellationToken); return session.ReadEventsAsync(cancellationToken);
} }
/// <summary> /// <inheritdoc />
/// Closes a gateway session asynchronously.
/// </summary>
/// <param name="sessionId">Session identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Session close result.</returns>
public async Task<SessionCloseResult> CloseSessionAsync( public async Task<SessionCloseResult> CloseSessionAsync(
string sessionId, string sessionId,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@@ -203,16 +176,7 @@ public sealed class SessionManager : ISessionManager
return result; return result;
} }
/// <summary> /// <inheritdoc />
/// Forcefully terminates a session's worker without attempting graceful shutdown.
/// Mirrors the registry/metrics cleanup that <see cref="CloseSessionCoreAsync"/>
/// performs after a successful close, but skips the <c>WorkerClient.ShutdownAsync</c>
/// step that <see cref="GatewaySession.CloseAsync"/> would otherwise attempt.
/// </summary>
/// <param name="sessionId">Session identifier.</param>
/// <param name="reason">Reason recorded for the kill.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Session close result.</returns>
public async Task<SessionCloseResult> KillWorkerAsync( public async Task<SessionCloseResult> KillWorkerAsync(
string sessionId, string sessionId,
string reason, string reason,
@@ -263,12 +227,7 @@ public sealed class SessionManager : ISessionManager
return new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: wasClosed); return new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: wasClosed);
} }
/// <summary> /// <inheritdoc />
/// Closes all sessions with expired leases asynchronously.
/// </summary>
/// <param name="now">Current time for lease expiration check.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Count of sessions closed.</returns>
public async Task<int> CloseExpiredLeasesAsync( public async Task<int> CloseExpiredLeasesAsync(
DateTimeOffset now, DateTimeOffset now,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@@ -288,11 +247,7 @@ public sealed class SessionManager : ISessionManager
return closedCount; return closedCount;
} }
/// <summary> /// <inheritdoc />
/// Shuts down all active sessions gracefully asynchronously.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Completed task.</returns>
public async Task ShutdownAsync(CancellationToken cancellationToken) public async Task ShutdownAsync(CancellationToken cancellationToken)
{ {
foreach (GatewaySession session in _registry.Snapshot()) foreach (GatewaySession session in _registry.Snapshot())
@@ -11,6 +11,7 @@ public sealed record SessionOpenRequest(
{ {
/// <summary>Creates a SessionOpenRequest from a gRPC OpenSessionRequest contract.</summary> /// <summary>Creates a SessionOpenRequest from a gRPC OpenSessionRequest contract.</summary>
/// <param name="request">Request payload.</param> /// <param name="request">Request payload.</param>
/// <returns>A new <see cref="SessionOpenRequest"/> populated from the contract fields.</returns>
public static SessionOpenRequest FromContract(OpenSessionRequest request) public static SessionOpenRequest FromContract(OpenSessionRequest request)
{ {
ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(request);
@@ -11,20 +11,13 @@ public sealed class SessionRegistry : ISessionRegistry
{ {
private readonly ConcurrentDictionary<string, GatewaySession> _sessions = new(StringComparer.Ordinal); private readonly ConcurrentDictionary<string, GatewaySession> _sessions = new(StringComparer.Ordinal);
/// <summary> /// <inheritdoc />
/// Gets the total count of sessions in the registry.
/// </summary>
public int Count => _sessions.Count; public int Count => _sessions.Count;
/// <summary> /// <inheritdoc />
/// Gets the count of non-closed sessions.
/// </summary>
public int ActiveCount => _sessions.Values.Count(session => session.State is not SessionState.Closed); public int ActiveCount => _sessions.Values.Count(session => session.State is not SessionState.Closed);
/// <summary> /// <inheritdoc />
/// Adds a session to the registry.
/// </summary>
/// <param name="session">Gateway session to add.</param>
public bool TryAdd(GatewaySession session) public bool TryAdd(GatewaySession session)
{ {
ArgumentNullException.ThrowIfNull(session); ArgumentNullException.ThrowIfNull(session);
@@ -32,11 +25,7 @@ public sealed class SessionRegistry : ISessionRegistry
return _sessions.TryAdd(session.SessionId, session); return _sessions.TryAdd(session.SessionId, session);
} }
/// <summary> /// <inheritdoc />
/// Retrieves a session by identifier.
/// </summary>
/// <param name="sessionId">Identifier of the session.</param>
/// <param name="session">The retrieved session if found.</param>
public bool TryGet( public bool TryGet(
string sessionId, string sessionId,
[MaybeNullWhen(false)] out GatewaySession session) [MaybeNullWhen(false)] out GatewaySession session)
@@ -44,11 +33,7 @@ public sealed class SessionRegistry : ISessionRegistry
return _sessions.TryGetValue(sessionId, out session); return _sessions.TryGetValue(sessionId, out session);
} }
/// <summary> /// <inheritdoc />
/// Removes a session from the registry by identifier.
/// </summary>
/// <param name="sessionId">Identifier of the session.</param>
/// <param name="session">The removed session if found.</param>
public bool TryRemove( public bool TryRemove(
string sessionId, string sessionId,
[MaybeNullWhen(false)] out GatewaySession session) [MaybeNullWhen(false)] out GatewaySession session)
@@ -56,9 +41,7 @@ public sealed class SessionRegistry : ISessionRegistry
return _sessions.TryRemove(sessionId, out session); return _sessions.TryRemove(sessionId, out session);
} }
/// <summary> /// <inheritdoc />
/// Returns a snapshot of all sessions in the registry.
/// </summary>
public IReadOnlyCollection<GatewaySession> Snapshot() public IReadOnlyCollection<GatewaySession> Snapshot()
{ {
return _sessions.Values.ToArray(); return _sessions.Values.ToArray();
@@ -8,13 +8,17 @@ public sealed class SessionShutdownHostedService(
ISessionManager sessionManager, ISessionManager sessionManager,
ILogger<SessionShutdownHostedService> logger) : IHostedService ILogger<SessionShutdownHostedService> logger) : IHostedService
{ {
/// <inheritdoc /> /// <summary>No-op start handler; this service only acts on shutdown.</summary>
/// <param name="cancellationToken">Cancellation token (unused).</param>
/// <returns>A completed task.</returns>
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <inheritdoc /> /// <summary>Shuts down all active gateway sessions gracefully.</summary>
/// <param name="cancellationToken">Token signaled when the host shutdown deadline is reached.</param>
/// <returns>A task that represents the asynchronous shutdown operation.</returns>
public async Task StopAsync(CancellationToken cancellationToken) public async Task StopAsync(CancellationToken cancellationToken)
{ {
try try
@@ -39,10 +39,7 @@ public sealed class SessionWorkerClientFactory : ISessionWorkerClientFactory
_options = options.Value; _options = options.Value;
} }
/// <summary>Creates a worker client and launches the worker process.</summary> /// <inheritdoc />
/// <param name="session">The gateway session.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The created worker client.</returns>
public async Task<IWorkerClient> CreateAsync( public async Task<IWorkerClient> CreateAsync(
GatewaySession session, GatewaySession session,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@@ -19,12 +19,14 @@ public interface IWorkerClient : IAsyncDisposable
/// <summary>Initiates the handshake and enters ready state.</summary> /// <summary>Initiates the handshake and enters ready state.</summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task StartAsync(CancellationToken cancellationToken); Task StartAsync(CancellationToken cancellationToken);
/// <summary>Sends a command to the worker and waits for a reply.</summary> /// <summary>Sends a command to the worker and waits for a reply.</summary>
/// <param name="command">Worker command to invoke.</param> /// <param name="command">Worker command to invoke.</param>
/// <param name="timeout">Timeout for waiting for the reply.</param> /// <param name="timeout">Timeout for waiting for the reply.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that resolves to the worker command reply.</returns>
Task<WorkerCommandReply> InvokeAsync( Task<WorkerCommandReply> InvokeAsync(
WorkerCommand command, WorkerCommand command,
TimeSpan timeout, TimeSpan timeout,
@@ -32,11 +34,13 @@ public interface IWorkerClient : IAsyncDisposable
/// <summary>Reads events from the worker as they arrive.</summary> /// <summary>Reads events from the worker as they arrive.</summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>An async sequence of worker events.</returns>
IAsyncEnumerable<WorkerEvent> ReadEventsAsync(CancellationToken cancellationToken); IAsyncEnumerable<WorkerEvent> ReadEventsAsync(CancellationToken cancellationToken);
/// <summary>Gracefully shuts down the worker by closing the connection.</summary> /// <summary>Gracefully shuts down the worker by closing the connection.</summary>
/// <param name="timeout">Timeout for shutdown.</param> /// <param name="timeout">Timeout for shutdown.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken); Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken);
/// <summary>Terminates the worker process immediately with a diagnostic reason.</summary> /// <summary>Terminates the worker process immediately with a diagnostic reason.</summary>
@@ -24,6 +24,7 @@ public interface IWorkerProcess : IDisposable
/// Waits for the process to exit with the specified cancellation token. /// Waits for the process to exit with the specified cancellation token.
/// </summary> /// </summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A value task that completes when the process exits.</returns>
ValueTask WaitForExitAsync(CancellationToken cancellationToken); ValueTask WaitForExitAsync(CancellationToken cancellationToken);
/// <summary> /// <summary>
@@ -8,7 +8,9 @@ public sealed class OrphanWorkerCleanupHostedService(
OrphanWorkerTerminator terminator, OrphanWorkerTerminator terminator,
ILogger<OrphanWorkerCleanupHostedService> logger) : IHostedService ILogger<OrphanWorkerCleanupHostedService> logger) : IHostedService
{ {
/// <inheritdoc /> /// <summary>Terminates any orphaned MXAccess worker processes found at startup.</summary>
/// <param name="cancellationToken">Token to observe for cancellation (unused; cleanup is best-effort).</param>
/// <returns>A completed task.</returns>
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
try try
@@ -25,6 +27,8 @@ public sealed class OrphanWorkerCleanupHostedService(
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <inheritdoc /> /// <summary>Performs no action on stop; all cleanup runs at startup.</summary>
/// <param name="cancellationToken">Token to observe for cancellation (unused).</param>
/// <returns>A completed task.</returns>
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
} }
@@ -28,7 +28,7 @@ internal sealed class SystemWorkerProcess(Process process) : IWorkerProcess
process.Kill(entireProcessTree); process.Kill(entireProcessTree);
} }
/// <inheritdoc /> /// <summary>Releases the underlying process resources.</summary>
public void Dispose() public void Dispose()
{ {
process.Dispose(); process.Dispose();
@@ -78,10 +78,10 @@ public sealed class WorkerClient : IWorkerClient
_lastHeartbeatAt = _timeProvider.GetUtcNow(); _lastHeartbeatAt = _timeProvider.GetUtcNow();
} }
/// <summary>Gets the worker's session ID.</summary> /// <inheritdoc />
public string SessionId => _connection.SessionId; public string SessionId => _connection.SessionId;
/// <summary>Gets the worker process ID.</summary> /// <inheritdoc />
public int? ProcessId public int? ProcessId
{ {
get get
@@ -93,7 +93,7 @@ public sealed class WorkerClient : IWorkerClient
} }
} }
/// <summary>Gets the current client state.</summary> /// <inheritdoc />
public WorkerClientState State public WorkerClientState State
{ {
get get
@@ -105,7 +105,7 @@ public sealed class WorkerClient : IWorkerClient
} }
} }
/// <summary>Gets the timestamp of the last received heartbeat.</summary> /// <inheritdoc />
public DateTimeOffset LastHeartbeatAt public DateTimeOffset LastHeartbeatAt
{ {
get get
@@ -117,8 +117,7 @@ public sealed class WorkerClient : IWorkerClient
} }
} }
/// <summary>Starts the worker client and completes the handshake.</summary> /// <inheritdoc />
/// <param name="cancellationToken">Cancellation token.</param>
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
ThrowIfDisposed(); ThrowIfDisposed();
@@ -141,11 +140,7 @@ public sealed class WorkerClient : IWorkerClient
_heartbeatLoopTask = Task.Run(HeartbeatLoopAsync); _heartbeatLoopTask = Task.Run(HeartbeatLoopAsync);
} }
/// <summary>Invokes a command on the worker and waits for reply.</summary> /// <inheritdoc />
/// <param name="command">The command to invoke.</param>
/// <param name="timeout">Command timeout.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The command reply.</returns>
public async Task<WorkerCommandReply> InvokeAsync( public async Task<WorkerCommandReply> InvokeAsync(
WorkerCommand command, WorkerCommand command,
TimeSpan timeout, TimeSpan timeout,
@@ -228,9 +223,7 @@ public sealed class WorkerClient : IWorkerClient
} }
} }
/// <summary>Reads events from the worker as an async stream.</summary> /// <inheritdoc />
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Async enumerable of worker events.</returns>
public async IAsyncEnumerable<WorkerEvent> ReadEventsAsync( public async IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken) [EnumeratorCancellation] CancellationToken cancellationToken)
{ {
@@ -242,9 +235,7 @@ public sealed class WorkerClient : IWorkerClient
} }
} }
/// <summary>Shuts down the worker gracefully.</summary> /// <inheritdoc />
/// <param name="timeout">Shutdown timeout.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken) public async Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken)
{ {
ThrowIfDisposed(); ThrowIfDisposed();
@@ -289,8 +280,7 @@ public sealed class WorkerClient : IWorkerClient
} }
} }
/// <summary>Terminates the worker process immediately.</summary> /// <inheritdoc />
/// <param name="reason">Reason for termination.</param>
public void Kill(string reason) public void Kill(string reason)
{ {
ThrowIfDisposed(); ThrowIfDisposed();
@@ -302,6 +292,7 @@ public sealed class WorkerClient : IWorkerClient
} }
/// <summary>Disposes the worker client and releases resources.</summary> /// <summary>Disposes the worker client and releases resources.</summary>
/// <returns>A task that represents the asynchronous dispose operation.</returns>
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
if (_disposed) if (_disposed)
@@ -30,6 +30,7 @@ public sealed class WorkerFrameWriter
/// </summary> /// </summary>
/// <param name="envelope">Worker envelope message to write.</param> /// <param name="envelope">Worker envelope message to write.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A value task that represents the asynchronous operation.</returns>
public async ValueTask WriteAsync( public async ValueTask WriteAsync(
WorkerEnvelope envelope, WorkerEnvelope envelope,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
@@ -30,7 +30,7 @@ public sealed class WorkerProcessHandle : IDisposable
/// <summary>Gets the time when the process was launched.</summary> /// <summary>Gets the time when the process was launched.</summary>
public DateTimeOffset LaunchedAt { get; } public DateTimeOffset LaunchedAt { get; }
/// <inheritdoc /> /// <summary>Releases the underlying worker process resources.</summary>
public void Dispose() public void Dispose()
{ {
Process.Dispose(); Process.Dispose();
@@ -58,12 +58,7 @@ public sealed class WorkerProcessLauncher : IWorkerProcessLauncher
_logger = logger ?? NullLogger<WorkerProcessLauncher>.Instance; _logger = logger ?? NullLogger<WorkerProcessLauncher>.Instance;
} }
/// <summary> /// <inheritdoc />
/// Launches a worker process and waits for startup.
/// </summary>
/// <param name="request">Request payload.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>Handle to the launched worker process.</returns>
public async Task<WorkerProcessHandle> LaunchAsync( public async Task<WorkerProcessHandle> LaunchAsync(
WorkerProcessLaunchRequest request, WorkerProcessLaunchRequest request,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
@@ -2,11 +2,7 @@ namespace ZB.MOM.WW.MxGateway.Server.Workers;
public sealed class WorkerProcessStartedProbe : IWorkerStartupProbe public sealed class WorkerProcessStartedProbe : IWorkerStartupProbe
{ {
/// <summary>Verifies that the worker process has started and has not exited.</summary> /// <inheritdoc />
/// <param name="process">Worker process to verify.</param>
/// <param name="request">Process launch request.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>Completed task if process is running.</returns>
public Task WaitUntilReadyAsync( public Task WaitUntilReadyAsync(
IWorkerProcess process, IWorkerProcess process,
WorkerProcessLaunchRequest request, WorkerProcessLaunchRequest request,
@@ -5,6 +5,7 @@ public static class WorkerServiceCollectionExtensions
{ {
/// <summary>Registers worker process launcher and factory services.</summary> /// <summary>Registers worker process launcher and factory services.</summary>
/// <param name="services">Service collection to register services.</param> /// <param name="services">Service collection to register services.</param>
/// <returns>The same <see cref="IServiceCollection"/> to allow chaining.</returns>
public static IServiceCollection AddWorkerProcessLauncher(this IServiceCollection services) public static IServiceCollection AddWorkerProcessLauncher(this IServiceCollection services)
{ {
services.AddSingleton<IWorkerProcessFactory, SystemWorkerProcessFactory>(); services.AddSingleton<IWorkerProcessFactory, SystemWorkerProcessFactory>();
@@ -46,6 +46,7 @@ public sealed class GatewayOptionsValidatorTests
Tls = tls, Tls = tls,
}; };
/// <summary>Verifies that the validator succeeds when TLS options are left at their defaults.</summary>
[Fact] [Fact]
public void Validate_Succeeds_WithDefaultTlsOptions() public void Validate_Succeeds_WithDefaultTlsOptions()
{ {
@@ -53,6 +54,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.True(result.Succeeded); Assert.True(result.Succeeded);
} }
/// <summary>Verifies that the validator fails when TLS validity years is set to zero.</summary>
[Fact] [Fact]
public void Validate_Fails_WhenTlsValidityYearsOutOfRange() public void Validate_Fails_WhenTlsValidityYearsOutOfRange()
{ {
@@ -62,6 +64,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:ValidityYears")); Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:ValidityYears"));
} }
/// <summary>Verifies that the validator fails when TLS validity years exceeds the allowed maximum.</summary>
[Fact] [Fact]
public void Validate_Fails_WhenTlsValidityYearsTooLarge() public void Validate_Fails_WhenTlsValidityYearsTooLarge()
{ {
@@ -71,6 +74,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:ValidityYears")); Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:ValidityYears"));
} }
/// <summary>Verifies that the validator fails when an additional DNS name is blank or whitespace.</summary>
[Fact] [Fact]
public void Validate_Fails_WhenAdditionalDnsNameBlank() public void Validate_Fails_WhenAdditionalDnsNameBlank()
{ {
@@ -80,6 +84,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:AdditionalDnsNames")); Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:AdditionalDnsNames"));
} }
/// <summary>Verifies that the validator fails when the self-signed certificate path is blank.</summary>
[Fact] [Fact]
public void Validate_Fails_WhenSelfSignedCertPathBlank() public void Validate_Fails_WhenSelfSignedCertPathBlank()
{ {
@@ -89,6 +94,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:SelfSignedCertPath must not be blank.")); Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:SelfSignedCertPath must not be blank."));
} }
/// <summary>Verifies that the validator fails when LDAP is enabled with port zero.</summary>
[Fact] [Fact]
public void Validate_Fails_WhenLdapPortIsZero() public void Validate_Fails_WhenLdapPortIsZero()
{ {
@@ -100,6 +106,7 @@ public sealed class GatewayOptionsValidatorTests
f => f.Contains("MxGateway:Ldap:Port must be between 1 and 65535 (was 0)")); f => f.Contains("MxGateway:Ldap:Port must be between 1 and 65535 (was 0)"));
} }
/// <summary>Verifies that the validator fails when LDAP is enabled with a port number above 65535.</summary>
[Fact] [Fact]
public void Validate_Fails_WhenLdapPortExceedsMaximum() public void Validate_Fails_WhenLdapPortExceedsMaximum()
{ {
@@ -111,6 +118,7 @@ public sealed class GatewayOptionsValidatorTests
f => f.Contains("MxGateway:Ldap:Port must be between 1 and 65535 (was 70000)")); f => f.Contains("MxGateway:Ldap:Port must be between 1 and 65535 (was 70000)"));
} }
/// <summary>Verifies that the validator succeeds when LDAP is enabled with a valid port in range.</summary>
[Fact] [Fact]
public void Validate_Succeeds_WhenLdapEnabledWithValidPort() public void Validate_Succeeds_WhenLdapEnabledWithValidPort()
{ {
@@ -6,6 +6,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Configuration;
public sealed class TlsOptionsBindingTests public sealed class TlsOptionsBindingTests
{ {
/// <summary>Verifies that TLS option defaults are applied when the configuration section is absent.</summary>
[Fact] [Fact]
public void Defaults_AreApplied_WhenSectionAbsent() public void Defaults_AreApplied_WhenSectionAbsent()
{ {
@@ -16,6 +17,7 @@ public sealed class TlsOptionsBindingTests
Assert.False(string.IsNullOrWhiteSpace(options.SelfSignedCertPath)); Assert.False(string.IsNullOrWhiteSpace(options.SelfSignedCertPath));
} }
/// <summary>Verifies that TLS options bind correctly from the MxGateway:Tls configuration section.</summary>
[Fact] [Fact]
public void Binds_FromMxGatewayTlsSection() public void Binds_FromMxGatewayTlsSection()
{ {

Some files were not shown because too many files have changed in this diff Show More