feat(client): standardize connection outcomes (#13)
quality-gate / quality (push) Successful in 59s
quality-gate / quality (push) Successful in 59s
This commit is contained in:
@@ -83,6 +83,110 @@ public sealed class RendezvousJoinClientTests
|
||||
Assert.Contains("cursor=next%20page%2Bcursor", handler.Requests[1].Uri.Query, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OutcomeReportingUsesTheAttemptCapabilityAndCoarseElapsedBucket()
|
||||
{
|
||||
CreateJoinAttemptResponse attempt = CreateAttempt();
|
||||
RecordingHandler handler = new(JsonResponse(HttpStatusCode.OK, new ReportConnectionOutcomeResponse
|
||||
{
|
||||
Accepted = true,
|
||||
IsDuplicate = false,
|
||||
}));
|
||||
using HttpClient http = new(handler) { BaseAddress = new("http://rendezvous.test/") };
|
||||
RendezvousJoinClient client = new(http);
|
||||
RendezvousConnectionOutcome outcome = RendezvousConnectionOutcome.Create(
|
||||
ConnectionOutcomeKind.DirectConnectTimedOut,
|
||||
RendezvousConnectionOutcomeSource.LocalTraversal,
|
||||
RendezvousConnectionFailureCategory.DirectConnection,
|
||||
RendezvousConnectionPhase.DirectConnection,
|
||||
TimeSpan.FromSeconds(6));
|
||||
|
||||
RendezvousClientResult<ReportConnectionOutcomeResponse> result =
|
||||
await client.ReportOutcomeAsync(attempt, outcome);
|
||||
|
||||
Assert.True(result.IsSuccess, result.Message);
|
||||
RecordedRequest request = Assert.Single(handler.Requests);
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
Assert.Equal(
|
||||
attempt.ClientPunchCapability,
|
||||
request.Headers["X-Rendezvous-Client-Punch-Capability"]);
|
||||
Assert.Contains("\"outcome\":\"directConnectTimedOut\"", request.Body, StringComparison.Ordinal);
|
||||
Assert.Contains("\"elapsedBucket\":\"fiveToFifteenSeconds\"", request.Body, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("diagnostic", request.Body, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectionStartReturnsATypedServiceOutcomeInsteadOfAnUnboundedFailure()
|
||||
{
|
||||
RecordingHandler handler = new(JsonResponse(HttpStatusCode.NotFound, new ApiError
|
||||
{
|
||||
Code = RendezvousErrorCode.NotFound,
|
||||
Message = "listing unavailable",
|
||||
}));
|
||||
using HttpClient http = new(handler) { BaseAddress = new("http://rendezvous.test/") };
|
||||
RendezvousJoinClient client = new(http);
|
||||
NetworkEndpoint fallback = new()
|
||||
{
|
||||
AddressFamily = AddressFamilyKind.Ipv4,
|
||||
Address = "203.0.113.93",
|
||||
Port = 9_063,
|
||||
};
|
||||
|
||||
RendezvousConnectionStartResult result = await client.CreateConnectionAttemptAsync(
|
||||
new CreateJoinAttemptRequest
|
||||
{
|
||||
IdempotencyKey = "typed-start",
|
||||
GameId = new("space-game"),
|
||||
EnvironmentId = new("production"),
|
||||
ListingId = new(Guid.Parse("00000000-0000-0000-0000-000000000230")),
|
||||
ProtocolVersion = 7,
|
||||
},
|
||||
fallback);
|
||||
|
||||
Assert.True(result.IsCompleted);
|
||||
Assert.False(result.IsReadyForTraversal);
|
||||
Assert.Null(result.Attempt);
|
||||
Assert.Equal(ConnectionOutcomeKind.DirectoryNotFound, result.Outcome!.Kind);
|
||||
Assert.Equal("203.0.113.93", result.Outcome.DedicatedFallback!.Address);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectionStartReturnsCancelledForAPrecancelledCallerToken()
|
||||
{
|
||||
RecordingHandler handler = new();
|
||||
using HttpClient http = new(handler) { BaseAddress = new("http://rendezvous.test/") };
|
||||
RendezvousJoinClient client = new(http);
|
||||
using CancellationTokenSource cancellation = new();
|
||||
cancellation.Cancel();
|
||||
|
||||
RendezvousConnectionStartResult result = await client.CreateConnectionAttemptAsync(
|
||||
CreateRequest("cancelled-before-send"),
|
||||
cancellationToken: cancellation.Token);
|
||||
|
||||
Assert.Empty(handler.Requests);
|
||||
Assert.Equal(ConnectionOutcomeKind.Cancelled, result.Outcome!.Kind);
|
||||
Assert.Equal(RendezvousConnectionOutcomeSource.Caller, result.Outcome.Source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectionStartReturnsCancelledWhenCallerStopsASilentRequest()
|
||||
{
|
||||
CancellingHandler handler = new();
|
||||
using HttpClient http = new(handler) { BaseAddress = new("http://rendezvous.test/") };
|
||||
RendezvousJoinClient client = new(http);
|
||||
using CancellationTokenSource cancellation = new();
|
||||
Task<RendezvousConnectionStartResult> pending = client.CreateConnectionAttemptAsync(
|
||||
CreateRequest("cancelled-in-flight"),
|
||||
cancellationToken: cancellation.Token);
|
||||
await handler.Started.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
|
||||
await cancellation.CancelAsync();
|
||||
RendezvousConnectionStartResult result = await pending;
|
||||
|
||||
Assert.Equal(ConnectionOutcomeKind.Cancelled, result.Outcome!.Kind);
|
||||
Assert.Equal(RendezvousConnectionOutcomeSource.Caller, result.Outcome.Source);
|
||||
}
|
||||
|
||||
private static CreateJoinAttemptResponse CreateAttempt() => new()
|
||||
{
|
||||
AttemptId = new(Guid.Parse("00000000-0000-0000-0000-000000000202")),
|
||||
@@ -95,6 +199,15 @@ public sealed class RendezvousJoinClientTests
|
||||
ExpiresAt = new DateTimeOffset(2030, 1, 1, 0, 0, 30, TimeSpan.Zero),
|
||||
};
|
||||
|
||||
private static CreateJoinAttemptRequest CreateRequest(string idempotencyKey) => new()
|
||||
{
|
||||
IdempotencyKey = idempotencyKey,
|
||||
GameId = new("space-game"),
|
||||
EnvironmentId = new("production"),
|
||||
ListingId = new(Guid.Parse("00000000-0000-0000-0000-000000000230")),
|
||||
ProtocolVersion = 7,
|
||||
};
|
||||
|
||||
private static HostJoinAttempt CreateHostAttempt(string id) => new()
|
||||
{
|
||||
AttemptId = new(Guid.Parse(id)),
|
||||
@@ -137,6 +250,22 @@ public sealed class RendezvousJoinClientTests
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CancellingHandler : HttpMessageHandler
|
||||
{
|
||||
internal TaskCompletionSource Started { get; } = new(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_ = request;
|
||||
Started.TrySetResult();
|
||||
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
|
||||
throw new InvalidOperationException("The silent request unexpectedly completed.");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record RecordedRequest(
|
||||
HttpMethod Method,
|
||||
Uri Uri,
|
||||
|
||||
Reference in New Issue
Block a user