Add XML documentation across gateway, worker, and .NET client
This commit is contained in:
@@ -16,6 +16,7 @@ public sealed class EventStreamServiceTests
|
||||
{
|
||||
private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>Verifies that events from the worker stream maintain their original sequence order.</summary>
|
||||
[Fact]
|
||||
public async Task StreamEventsAsync_YieldsEventsInWorkerOrder()
|
||||
{
|
||||
@@ -36,6 +37,7 @@ public sealed class EventStreamServiceTests
|
||||
Assert.Equal(1, metrics.GetSnapshot().StreamDisconnects);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a second event subscriber is rejected when one is already active.</summary>
|
||||
[Fact]
|
||||
public async Task StreamEventsAsync_WhenSecondSubscriberStarts_RejectsClearly()
|
||||
{
|
||||
@@ -64,6 +66,7 @@ public sealed class EventStreamServiceTests
|
||||
await WaitUntilAsync(() => session.ActiveEventSubscriberCount == 0);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that canceling an event stream detaches the subscriber cleanly.</summary>
|
||||
[Fact]
|
||||
public async Task StreamEventsAsync_WhenCanceled_DetachesSubscriber()
|
||||
{
|
||||
@@ -85,6 +88,7 @@ public sealed class EventStreamServiceTests
|
||||
await WaitUntilAsync(() => session.ActiveEventSubscriberCount == 0);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that disposing an event stream with buffered events resets the queue depth metric.</summary>
|
||||
[Fact]
|
||||
public async Task StreamEventsAsync_WhenDisposedWithBufferedEvents_ResetsStreamQueueDepth()
|
||||
{
|
||||
@@ -111,6 +115,7 @@ public sealed class EventStreamServiceTests
|
||||
await WaitUntilAsync(() => metrics.GetSnapshot().GrpcEventStreamQueueDepth == 0);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that queue depth metrics correctly track concurrent event streams across multiple sessions.</summary>
|
||||
[Fact]
|
||||
public async Task StreamEventsAsync_WithConcurrentStreams_TracksAggregateQueueDepth()
|
||||
{
|
||||
@@ -151,6 +156,7 @@ public sealed class EventStreamServiceTests
|
||||
await WaitUntilAsync(() => metrics.GetSnapshot().GrpcEventStreamQueueDepth == 0);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that event queue overflow faults the session and reports the overflow metric.</summary>
|
||||
[Fact]
|
||||
public async Task StreamEventsAsync_WhenStreamQueueOverflows_FaultsSessionAndReportsOverflow()
|
||||
{
|
||||
@@ -180,6 +186,7 @@ public sealed class EventStreamServiceTests
|
||||
Assert.Equal(1, metrics.GetSnapshot().Faults);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the disconnect backpressure policy disconnects the subscriber without faulting the session.</summary>
|
||||
[Fact]
|
||||
public async Task StreamEventsAsync_WhenStreamQueueOverflowsWithDisconnectPolicy_LeavesSessionReady()
|
||||
{
|
||||
@@ -211,6 +218,7 @@ public sealed class EventStreamServiceTests
|
||||
Assert.Equal(1, snapshot.StreamDisconnects);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the event stream does not synthesize OperationComplete events from write completions.</summary>
|
||||
[Fact]
|
||||
public async Task StreamEventsAsync_DoesNotSynthesizeOperationComplete()
|
||||
{
|
||||
@@ -227,6 +235,7 @@ public sealed class EventStreamServiceTests
|
||||
Assert.DoesNotContain(events, candidate => candidate.Family == MxEventFamily.OperationComplete);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a terminal fault from the worker event stream propagates and faults the session.</summary>
|
||||
[Fact]
|
||||
public async Task StreamEventsAsync_WhenWorkerEventStreamFaults_PropagatesTerminalFault()
|
||||
{
|
||||
@@ -359,15 +368,19 @@ public sealed class EventStreamServiceTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fake session manager for testing event streams.</summary>
|
||||
private sealed class FakeSessionManager : ISessionManager
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, GatewaySession> _sessions;
|
||||
|
||||
/// <summary>Initializes a new instance of the FakeSessionManager.</summary>
|
||||
/// <param name="sessions">Sessions to manage.</param>
|
||||
public FakeSessionManager(params GatewaySession[] sessions)
|
||||
{
|
||||
_sessions = sessions.ToDictionary(session => session.SessionId, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<GatewaySession> OpenSessionAsync(
|
||||
SessionOpenRequest request,
|
||||
string? clientIdentity,
|
||||
@@ -376,6 +389,7 @@ public sealed class EventStreamServiceTests
|
||||
return Task.FromResult(_sessions.Values.First());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetSession(
|
||||
string sessionId,
|
||||
out GatewaySession gatewaySession)
|
||||
@@ -383,6 +397,7 @@ public sealed class EventStreamServiceTests
|
||||
return _sessions.TryGetValue(sessionId, out gatewaySession!);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<WorkerCommandReply> InvokeAsync(
|
||||
string sessionId,
|
||||
WorkerCommand command,
|
||||
@@ -391,6 +406,7 @@ public sealed class EventStreamServiceTests
|
||||
return Task.FromResult(new WorkerCommandReply());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
|
||||
string sessionId,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -398,6 +414,7 @@ public sealed class EventStreamServiceTests
|
||||
return _sessions[sessionId].ReadEventsAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<SessionCloseResult> CloseSessionAsync(
|
||||
string sessionId,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -405,6 +422,7 @@ public sealed class EventStreamServiceTests
|
||||
return Task.FromResult(new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: false));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> CloseExpiredLeasesAsync(
|
||||
DateTimeOffset now,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -412,33 +430,44 @@ public sealed class EventStreamServiceTests
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ShutdownAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fake worker client for testing event streams.</summary>
|
||||
private sealed class FakeWorkerClient : IWorkerClient
|
||||
{
|
||||
/// <summary>Gets the list of queued worker events.</summary>
|
||||
public List<WorkerEvent> Events { get; } = [];
|
||||
|
||||
/// <summary>Gets or sets whether to complete the event stream after configured events are yielded.</summary>
|
||||
public bool CompleteAfterConfiguredEvents { get; set; }
|
||||
|
||||
/// <summary>Gets or sets an optional exception to throw as a terminal event stream fault.</summary>
|
||||
public Exception? TerminalException { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SessionId { get; } = "session-events";
|
||||
|
||||
/// <inheritdoc />
|
||||
public int? ProcessId { get; } = 4321;
|
||||
|
||||
/// <inheritdoc />
|
||||
public WorkerClientState State { get; private set; } = WorkerClientState.Ready;
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<WorkerCommandReply> InvokeAsync(
|
||||
WorkerCommand command,
|
||||
TimeSpan timeout,
|
||||
@@ -447,6 +476,7 @@ public sealed class EventStreamServiceTests
|
||||
return Task.FromResult(new WorkerCommandReply());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -469,6 +499,7 @@ public sealed class EventStreamServiceTests
|
||||
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ShutdownAsync(
|
||||
TimeSpan timeout,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -477,11 +508,13 @@ public sealed class EventStreamServiceTests
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Kill(string reason)
|
||||
{
|
||||
State = WorkerClientState.Faulted;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace MxGateway.Tests.Gateway.Grpc;
|
||||
|
||||
public sealed class MxAccessGatewayServiceTests
|
||||
{
|
||||
/// <summary>Verifies that OpenSession returns correct session details for a valid request.</summary>
|
||||
[Fact]
|
||||
public async Task OpenSession_WithValidRequest_ReturnsSessionDetails()
|
||||
{
|
||||
@@ -46,6 +47,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
Assert.Equal("operator-session", sessionManager.LastOpenRequest?.ClientSessionName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Invoke throws NotFound when the session does not exist.</summary>
|
||||
[Fact]
|
||||
public async Task Invoke_WhenSessionMissing_ThrowsNotFound()
|
||||
{
|
||||
@@ -66,6 +68,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
Assert.Contains("session-missing", exception.Status.Detail, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Invoke throws InvalidArgument and does not invoke the session manager when payload is mismatched.</summary>
|
||||
[Fact]
|
||||
public async Task Invoke_WithMismatchedPayload_ThrowsInvalidArgumentAndDoesNotCallSessionManager()
|
||||
{
|
||||
@@ -88,6 +91,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
Assert.Equal(0, sessionManager.InvokeCount);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Invoke returns HResult status and method payload from worker reply.</summary>
|
||||
[Fact]
|
||||
public async Task Invoke_WithWorkerReply_ReturnsHresultStatusAndMethodPayload()
|
||||
{
|
||||
@@ -142,6 +146,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
Assert.Equal("mxaccess diagnostic", reply.DiagnosticMessage);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that StreamEvents writes only events after the specified worker sequence.</summary>
|
||||
[Fact]
|
||||
public async Task StreamEvents_WithAfterSequence_WritesOnlyLaterEvents()
|
||||
{
|
||||
@@ -165,6 +170,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
Assert.Equal("session-1", sessionManager.LastReadEventsSessionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that StreamEvents records send duration metrics when an event is written.</summary>
|
||||
[Fact]
|
||||
public async Task StreamEvents_WhenEventIsWritten_RecordsSendDuration()
|
||||
{
|
||||
@@ -209,6 +215,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
Assert.Equal([MxEventFamily.OnDataChange.ToString()], families);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that CloseSession throws InvalidArgument when session ID is blank.</summary>
|
||||
[Fact]
|
||||
public async Task CloseSession_WithBlankSessionId_ThrowsInvalidArgument()
|
||||
{
|
||||
@@ -299,16 +306,22 @@ public sealed class MxAccessGatewayServiceTests
|
||||
|
||||
private sealed class FakeSessionManager : ISessionManager
|
||||
{
|
||||
/// <summary>The session to return from OpenSessionAsync.</summary>
|
||||
public GatewaySession? OpenSessionResult { get; init; }
|
||||
|
||||
/// <summary>The last OpenSessionAsync request captured.</summary>
|
||||
public SessionOpenRequest? LastOpenRequest { get; private set; }
|
||||
|
||||
/// <summary>The last client identity passed to OpenSessionAsync.</summary>
|
||||
public string? LastClientIdentity { get; private set; }
|
||||
|
||||
/// <summary>The last session ID passed to ReadEventsAsync.</summary>
|
||||
public string? LastReadEventsSessionId { get; private set; }
|
||||
|
||||
/// <summary>The last worker command passed to InvokeAsync.</summary>
|
||||
public WorkerCommand? LastWorkerCommand { get; private set; }
|
||||
|
||||
/// <summary>The reply to return from InvokeAsync.</summary>
|
||||
public WorkerCommandReply InvokeReply { get; init; } = new()
|
||||
{
|
||||
Reply = new MxCommandReply
|
||||
@@ -319,17 +332,23 @@ public sealed class MxAccessGatewayServiceTests
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>The exception to throw from InvokeAsync.</summary>
|
||||
public Exception? InvokeException { get; init; }
|
||||
|
||||
/// <summary>The number of times InvokeAsync was called.</summary>
|
||||
public int InvokeCount { get; private set; }
|
||||
|
||||
/// <summary>The events to return from ReadEventsAsync.</summary>
|
||||
public List<WorkerEvent> Events { get; } = [];
|
||||
|
||||
/// <summary>Records the session ID passed to ReadEventsAsync.</summary>
|
||||
/// <param name="sessionId">Identifier of the session.</param>
|
||||
public void RecordReadEventsSessionId(string sessionId)
|
||||
{
|
||||
LastReadEventsSessionId = sessionId;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<GatewaySession> OpenSessionAsync(
|
||||
SessionOpenRequest request,
|
||||
string? clientIdentity,
|
||||
@@ -341,6 +360,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
return Task.FromResult(OpenSessionResult ?? CreateSession("session-1", processId: 1234));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetSession(
|
||||
string sessionId,
|
||||
out GatewaySession session)
|
||||
@@ -349,6 +369,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<WorkerCommandReply> InvokeAsync(
|
||||
string sessionId,
|
||||
WorkerCommand command,
|
||||
@@ -365,6 +386,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
return Task.FromResult(InvokeReply);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
|
||||
string sessionId,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
@@ -378,6 +400,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<SessionCloseResult> CloseSessionAsync(
|
||||
string sessionId,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -385,6 +408,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
return Task.FromResult(new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: false));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> CloseExpiredLeasesAsync(
|
||||
DateTimeOffset now,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -392,6 +416,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ShutdownAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
@@ -400,6 +425,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
|
||||
private sealed class FakeEventStreamService(FakeSessionManager sessionManager) : IEventStreamService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||
StreamEventsRequest request,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
@@ -421,19 +447,25 @@ public sealed class MxAccessGatewayServiceTests
|
||||
|
||||
private sealed class FakeWorkerClient(int processId) : IWorkerClient
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string SessionId { get; } = "session-1";
|
||||
|
||||
/// <inheritdoc />
|
||||
public int? ProcessId { get; } = processId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public WorkerClientState State { get; } = WorkerClientState.Ready;
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<WorkerCommandReply> InvokeAsync(
|
||||
WorkerCommand command,
|
||||
TimeSpan timeout,
|
||||
@@ -442,6 +474,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
return Task.FromResult(new WorkerCommandReply());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<WorkerEvent> ReadEventsAsync(
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -449,6 +482,7 @@ public sealed class MxAccessGatewayServiceTests
|
||||
yield break;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ShutdownAsync(
|
||||
TimeSpan timeout,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -456,10 +490,12 @@ public sealed class MxAccessGatewayServiceTests
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Kill(string reason)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
@@ -468,10 +504,13 @@ public sealed class MxAccessGatewayServiceTests
|
||||
|
||||
private sealed class TestServerStreamWriter<T> : IServerStreamWriter<T>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public List<T> Messages { get; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public WriteOptions? WriteOptions { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task WriteAsync(T message)
|
||||
{
|
||||
Messages.Add(message);
|
||||
@@ -488,43 +527,56 @@ public sealed class MxAccessGatewayServiceTests
|
||||
private Status status;
|
||||
private WriteOptions? writeOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string MethodCore => "/mxaccess_gateway.v1.MxAccessGateway/Test";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string HostCore => "localhost";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string PeerCore => "ipv4:127.0.0.1:5000";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override DateTime DeadlineCore => DateTime.UtcNow.AddMinutes(1);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Metadata RequestHeadersCore => requestHeaders;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override CancellationToken CancellationTokenCore => cancellationToken;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Metadata ResponseTrailersCore => responseTrailers;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Status StatusCore
|
||||
{
|
||||
get => status;
|
||||
set => status = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override WriteOptions? WriteOptionsCore
|
||||
{
|
||||
get => writeOptions;
|
||||
set => writeOptions = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override AuthContext AuthContextCore { get; } = new(
|
||||
string.Empty,
|
||||
new Dictionary<string, List<AuthProperty>>(StringComparer.Ordinal));
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IDictionary<object, object> UserStateCore => userState;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ContextPropagationToken CreatePropagationTokenCore(
|
||||
ContextPropagationOptions? options)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace MxGateway.Tests.Gateway.Grpc;
|
||||
|
||||
public sealed class MxAccessGrpcMapperTests
|
||||
{
|
||||
/// <summary>Verifies that command mapping clones payloads to isolate them across process boundaries.</summary>
|
||||
[Fact]
|
||||
public void MapCommand_ClonesMethodSpecificPayloadForWorkerBoundary()
|
||||
{
|
||||
@@ -37,6 +38,7 @@ public sealed class MxAccessGrpcMapperTests
|
||||
Assert.NotNull(workerCommand.EnqueueTimestamp);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that command reply mapping preserves HRESULT and status information.</summary>
|
||||
[Fact]
|
||||
public void MapCommandReply_PreservesHresultStatusesAndPayload()
|
||||
{
|
||||
@@ -66,6 +68,7 @@ public sealed class MxAccessGrpcMapperTests
|
||||
Assert.Equal("denied", Assert.Single(publicReply.Statuses).DiagnosticText);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a missing worker reply returns a protocol violation status.</summary>
|
||||
[Fact]
|
||||
public void MapCommandReply_WhenWorkerReplyMissing_ReturnsProtocolViolationReply()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user