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:
@@ -3,6 +3,7 @@ using System.Net.Http.Json;
|
||||
using FinalFactory.Rendezvous.Client;
|
||||
using FinalFactory.Rendezvous.Contracts;
|
||||
using FinalFactory.Rendezvous.Server.Browser;
|
||||
using FinalFactory.Rendezvous.Server.ConnectionOutcomes;
|
||||
using FinalFactory.Rendezvous.Server.Http;
|
||||
using FinalFactory.Rendezvous.Server.JoinAttempts;
|
||||
using FinalFactory.Rendezvous.Server.Provisioning;
|
||||
@@ -103,6 +104,139 @@ public sealed class JoinAttemptHttpEndpointTests
|
||||
Assert.True(cancelledAttempt.IsCancelled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OutcomeReportingIsCapabilityAuthenticatedAndIdempotentOverHttp()
|
||||
{
|
||||
await using JoinHttpTestHost host = await JoinHttpTestHost.StartAsync();
|
||||
RendezvousPublisherClient publisher = new(host.HttpClient);
|
||||
PublishedSession session = AssertSuccess(await publisher.RegisterAsync(
|
||||
CreateRegistration(),
|
||||
host.PublisherCredential));
|
||||
Assert.True(host.Capabilities.TryFingerprint(
|
||||
session.HostPresenceCapability,
|
||||
out SecretFingerprint presenceFingerprint));
|
||||
Assert.True(host.Store.BindHostPresence(new(
|
||||
session.HostPresenceHandle,
|
||||
presenceFingerprint,
|
||||
new(AddressFamilyKind.Ipv4, "203.0.113.80", 41_000),
|
||||
null)).Succeeded);
|
||||
using HttpResponseMessage createdResponse = await host.HttpClient.PostAsJsonAsync(
|
||||
"v1/join-attempts",
|
||||
new CreateJoinAttemptRequest
|
||||
{
|
||||
IdempotencyKey = "outcome-report-1",
|
||||
GameId = new("space-game"),
|
||||
EnvironmentId = new("production"),
|
||||
ListingId = session.ListingId,
|
||||
ProtocolVersion = 7,
|
||||
},
|
||||
ContractJson.Options);
|
||||
CreateJoinAttemptResponse created = Assert.IsType<CreateJoinAttemptResponse>(
|
||||
await createdResponse.Content.ReadFromJsonAsync<CreateJoinAttemptResponse>(ContractJson.Options));
|
||||
ReportConnectionOutcomeRequest report = new()
|
||||
{
|
||||
Outcome = ConnectionOutcomeKind.PunchTimedOut,
|
||||
ElapsedBucket = ConnectionElapsedBucket.FiveToFifteenSeconds,
|
||||
};
|
||||
|
||||
ReportConnectionOutcomeResponse first = await SendOutcomeAsync(
|
||||
host.HttpClient,
|
||||
created,
|
||||
report);
|
||||
ReportConnectionOutcomeResponse duplicate = await SendOutcomeAsync(
|
||||
host.HttpClient,
|
||||
created,
|
||||
report);
|
||||
|
||||
Assert.True(first.Accepted);
|
||||
Assert.False(first.IsDuplicate);
|
||||
Assert.True(duplicate.Accepted);
|
||||
Assert.True(duplicate.IsDuplicate);
|
||||
Assert.Equal(
|
||||
1,
|
||||
host.OutcomeMetrics.GetCount(
|
||||
ConnectionOutcomeKind.PunchTimedOut,
|
||||
ConnectionElapsedBucket.FiveToFifteenSeconds));
|
||||
|
||||
using HttpRequestMessage conflictRequest = OutcomeRequest(
|
||||
created,
|
||||
new ReportConnectionOutcomeRequest
|
||||
{
|
||||
Outcome = ConnectionOutcomeKind.Connected,
|
||||
ElapsedBucket = ConnectionElapsedBucket.FiveToFifteenSeconds,
|
||||
});
|
||||
using HttpResponseMessage conflict = await host.HttpClient.SendAsync(conflictRequest);
|
||||
Assert.Equal(HttpStatusCode.Conflict, conflict.StatusCode);
|
||||
|
||||
using HttpRequestMessage unauthorizedRequest = OutcomeRequest(created, report);
|
||||
unauthorizedRequest.Headers.Remove("X-Rendezvous-Client-Punch-Capability");
|
||||
unauthorizedRequest.Headers.Add(
|
||||
"X-Rendezvous-Client-Punch-Capability",
|
||||
new string('X', ContractLimits.DerivedCredentialCharacters));
|
||||
using HttpResponseMessage unauthorized = await host.HttpClient.SendAsync(unauthorizedRequest);
|
||||
Assert.Equal(HttpStatusCode.NotFound, unauthorized.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(7u, HttpStatusCode.Gone, RendezvousErrorCode.StaleHost)]
|
||||
[InlineData(8u, HttpStatusCode.Conflict, RendezvousErrorCode.IncompatibleProtocol)]
|
||||
public async Task JoinCreationPreservesTypedTerminalErrorsOverHttp(
|
||||
uint protocolVersion,
|
||||
HttpStatusCode expectedStatus,
|
||||
RendezvousErrorCode expectedError)
|
||||
{
|
||||
await using JoinHttpTestHost host = await JoinHttpTestHost.StartAsync();
|
||||
RendezvousPublisherClient publisher = new(host.HttpClient);
|
||||
PublishedSession session = AssertSuccess(await publisher.RegisterAsync(
|
||||
CreateRegistration(),
|
||||
host.PublisherCredential));
|
||||
|
||||
using HttpResponseMessage response = await host.HttpClient.PostAsJsonAsync(
|
||||
"v1/join-attempts",
|
||||
new CreateJoinAttemptRequest
|
||||
{
|
||||
IdempotencyKey = $"typed-http-error-{protocolVersion}",
|
||||
GameId = new("space-game"),
|
||||
EnvironmentId = new("production"),
|
||||
ListingId = session.ListingId,
|
||||
ProtocolVersion = protocolVersion,
|
||||
},
|
||||
ContractJson.Options);
|
||||
|
||||
Assert.Equal(expectedStatus, response.StatusCode);
|
||||
ApiError error = Assert.IsType<ApiError>(
|
||||
await response.Content.ReadFromJsonAsync<ApiError>(ContractJson.Options));
|
||||
Assert.Equal(expectedError, error.Code);
|
||||
}
|
||||
|
||||
private static async Task<ReportConnectionOutcomeResponse> SendOutcomeAsync(
|
||||
HttpClient client,
|
||||
CreateJoinAttemptResponse attempt,
|
||||
ReportConnectionOutcomeRequest report)
|
||||
{
|
||||
using HttpRequestMessage request = OutcomeRequest(attempt, report);
|
||||
using HttpResponseMessage response = await client.SendAsync(request);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
return Assert.IsType<ReportConnectionOutcomeResponse>(
|
||||
await response.Content.ReadFromJsonAsync<ReportConnectionOutcomeResponse>(ContractJson.Options));
|
||||
}
|
||||
|
||||
private static HttpRequestMessage OutcomeRequest(
|
||||
CreateJoinAttemptResponse attempt,
|
||||
ReportConnectionOutcomeRequest report)
|
||||
{
|
||||
HttpRequestMessage request = new(
|
||||
HttpMethod.Post,
|
||||
$"v1/join-attempts/{attempt.AttemptId}/outcome")
|
||||
{
|
||||
Content = JsonContent.Create(report, options: ContractJson.Options),
|
||||
};
|
||||
request.Headers.Add(
|
||||
"X-Rendezvous-Client-Punch-Capability",
|
||||
attempt.ClientPunchCapability);
|
||||
return request;
|
||||
}
|
||||
|
||||
private static T AssertSuccess<T>(RendezvousClientResult<T> result)
|
||||
{
|
||||
Assert.True(result.IsSuccess, result.Message);
|
||||
@@ -132,18 +266,21 @@ public sealed class JoinAttemptHttpEndpointTests
|
||||
HttpClient httpClient,
|
||||
InMemoryEphemeralRendezvousStore store,
|
||||
EphemeralCapabilityIssuer capabilities,
|
||||
ConnectionOutcomeMetrics outcomeMetrics,
|
||||
string publisherCredential)
|
||||
{
|
||||
_application = application;
|
||||
HttpClient = httpClient;
|
||||
Store = store;
|
||||
Capabilities = capabilities;
|
||||
OutcomeMetrics = outcomeMetrics;
|
||||
PublisherCredential = publisherCredential;
|
||||
}
|
||||
|
||||
internal HttpClient HttpClient { get; }
|
||||
internal InMemoryEphemeralRendezvousStore Store { get; }
|
||||
internal EphemeralCapabilityIssuer Capabilities { get; }
|
||||
internal ConnectionOutcomeMetrics OutcomeMetrics { get; }
|
||||
internal string PublisherCredential { get; }
|
||||
|
||||
internal static async Task<JoinHttpTestHost> StartAsync()
|
||||
@@ -181,6 +318,9 @@ public sealed class JoinAttemptHttpEndpointTests
|
||||
builder.Services.AddSingleton<SessionBrowserService>();
|
||||
builder.Services.AddSingleton<JoinAttemptCursorCodec>();
|
||||
builder.Services.AddSingleton<JoinAttemptService>();
|
||||
ConnectionOutcomeMetrics outcomeMetrics = new();
|
||||
builder.Services.AddSingleton(outcomeMetrics);
|
||||
builder.Services.AddSingleton<ConnectionOutcomeService>();
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
app.UseExceptionHandler();
|
||||
@@ -193,6 +333,7 @@ public sealed class JoinAttemptHttpEndpointTests
|
||||
new HttpClient { BaseAddress = new Uri(address) },
|
||||
store,
|
||||
capabilities,
|
||||
outcomeMetrics,
|
||||
credential);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user