155 lines
6.6 KiB
C#
155 lines
6.6 KiB
C#
using System.Net;
|
|
using System.Text.Json;
|
|
using FinalFactory.Rendezvous.Client;
|
|
using FinalFactory.Rendezvous.Contracts;
|
|
|
|
namespace FinalFactory.Rendezvous.Tests.Client;
|
|
|
|
public sealed class RendezvousJoinClientTests
|
|
{
|
|
[Fact]
|
|
public async Task JoinIssuanceRetriesTheSameIdempotentPayloadAndCancellationUsesCapability()
|
|
{
|
|
CreateJoinAttemptResponse created = CreateAttempt();
|
|
RecordingHandler handler = new(
|
|
new HttpResponseMessage(HttpStatusCode.ServiceUnavailable),
|
|
JsonResponse(HttpStatusCode.Created, created),
|
|
new HttpResponseMessage(HttpStatusCode.NoContent));
|
|
using HttpClient http = new(handler) { BaseAddress = new("http://rendezvous.test/") };
|
|
RendezvousJoinClient client = new(
|
|
http,
|
|
new RendezvousClientOptions { JitterRatio = 0 },
|
|
new ImmediateDelay());
|
|
CreateJoinAttemptRequest request = new()
|
|
{
|
|
IdempotencyKey = "stable-join-key",
|
|
GameId = new("space-game"),
|
|
EnvironmentId = new("production"),
|
|
ListingId = new(Guid.Parse("00000000-0000-0000-0000-000000000201")),
|
|
ProtocolVersion = 7,
|
|
};
|
|
|
|
RendezvousClientResult<CreateJoinAttemptResponse> result = await client.CreateAsync(request);
|
|
RendezvousClientResult<bool> cancelled = await client.CancelAsync(created);
|
|
|
|
Assert.True(result.IsSuccess, result.Message);
|
|
Assert.True(cancelled.IsSuccess, cancelled.Message);
|
|
Assert.Equal(handler.Requests[0].Body, handler.Requests[1].Body);
|
|
Assert.Contains("stable-join-key", handler.Requests[0].Body, StringComparison.Ordinal);
|
|
RecordedRequest cancellation = handler.Requests[2];
|
|
Assert.Equal(HttpMethod.Delete, cancellation.Method);
|
|
Assert.Equal(
|
|
created.ClientPunchCapability,
|
|
cancellation.Headers["X-Rendezvous-Client-Punch-Capability"]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task HostInvitationPollingFollowsCursorsWithTheLeaseToken()
|
|
{
|
|
HostJoinAttempt first = CreateHostAttempt("00000000-0000-0000-0000-000000000211");
|
|
HostJoinAttempt second = CreateHostAttempt("00000000-0000-0000-0000-000000000212");
|
|
RecordingHandler handler = new(
|
|
JsonResponse(HttpStatusCode.OK, new BrowseHostJoinAttemptsResponse
|
|
{
|
|
Items = [first],
|
|
NextCursor = "next page+cursor",
|
|
}),
|
|
JsonResponse(HttpStatusCode.OK, new BrowseHostJoinAttemptsResponse
|
|
{
|
|
Items = [second],
|
|
}));
|
|
using HttpClient http = new(handler) { BaseAddress = new("http://rendezvous.test/") };
|
|
RendezvousJoinClient client = new(http);
|
|
PublishedSession session = new(new RegisterSessionResponse
|
|
{
|
|
ListingId = new(Guid.Parse("00000000-0000-0000-0000-000000000220")),
|
|
LeaseId = new(Guid.Parse("00000000-0000-0000-0000-000000000221")),
|
|
LeaseToken = "lease-secret",
|
|
HostPresenceHandle = new(Guid.Parse("00000000-0000-0000-0000-000000000222")),
|
|
HostPresenceCapability = Credential('P'),
|
|
ExpiresAt = new DateTimeOffset(2030, 1, 1, 0, 0, 0, TimeSpan.Zero),
|
|
LeaseRenewAfterSeconds = 15,
|
|
HostPresenceRefreshAfterSeconds = 10,
|
|
});
|
|
|
|
RendezvousClientResult<IReadOnlyList<HostJoinAttempt>> result =
|
|
await client.BrowseAllForHostAsync(session);
|
|
|
|
Assert.True(result.IsSuccess, result.Message);
|
|
Assert.Equal([first.AttemptId, second.AttemptId], result.Value!.Select(item => item.AttemptId));
|
|
Assert.Equal(2, handler.Requests.Count);
|
|
Assert.All(handler.Requests, request =>
|
|
Assert.Equal("lease-secret", request.Headers["X-Rendezvous-Lease-Token"]));
|
|
Assert.Contains("cursor=next%20page%2Bcursor", handler.Requests[1].Uri.Query, StringComparison.Ordinal);
|
|
}
|
|
|
|
private static CreateJoinAttemptResponse CreateAttempt() => new()
|
|
{
|
|
AttemptId = new(Guid.Parse("00000000-0000-0000-0000-000000000202")),
|
|
MediationHandle = new(Guid.Parse("00000000-0000-0000-0000-000000000203")),
|
|
ClientPunchCapability = Credential('C'),
|
|
ConnectionTicketDigest = NatIntroductionTokenCodec.ComputeDigest(
|
|
NatIntroductionTokenCodec.Encode(
|
|
new JoinAttemptId(Guid.Parse("00000000-0000-0000-0000-000000000202")),
|
|
Credential('T'))),
|
|
ExpiresAt = new DateTimeOffset(2030, 1, 1, 0, 0, 30, TimeSpan.Zero),
|
|
};
|
|
|
|
private static HostJoinAttempt CreateHostAttempt(string id) => new()
|
|
{
|
|
AttemptId = new(Guid.Parse(id)),
|
|
MediationHandle = new(Guid.NewGuid()),
|
|
HostPunchCapability = Credential('H'),
|
|
ConnectionTicketDigest = NatIntroductionTokenCodec.ComputeDigest(
|
|
NatIntroductionTokenCodec.Encode(new JoinAttemptId(Guid.Parse(id)), Credential('T'))),
|
|
ExpiresAt = new DateTimeOffset(2030, 1, 1, 0, 0, 30, TimeSpan.Zero),
|
|
};
|
|
|
|
private static string Credential(char value) => new(value, ContractLimits.DerivedCredentialCharacters);
|
|
|
|
private static HttpResponseMessage JsonResponse<T>(HttpStatusCode status, T value) => new(status)
|
|
{
|
|
Content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(value, ContractJson.Options)),
|
|
};
|
|
|
|
private sealed class RecordingHandler(params HttpResponseMessage[] responses) : HttpMessageHandler
|
|
{
|
|
private readonly Queue<HttpResponseMessage> _responses = new(responses);
|
|
|
|
internal List<RecordedRequest> Requests { get; } = [];
|
|
|
|
protected override async Task<HttpResponseMessage> SendAsync(
|
|
HttpRequestMessage request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
Dictionary<string, string> headers = request.Headers.ToDictionary(
|
|
static item => item.Key,
|
|
static item => string.Join(",", item.Value),
|
|
StringComparer.OrdinalIgnoreCase);
|
|
Requests.Add(new(
|
|
request.Method,
|
|
request.RequestUri!,
|
|
headers,
|
|
request.Content is null
|
|
? string.Empty
|
|
: await request.Content.ReadAsStringAsync(cancellationToken)));
|
|
return _responses.Dequeue();
|
|
}
|
|
}
|
|
|
|
private sealed record RecordedRequest(
|
|
HttpMethod Method,
|
|
Uri Uri,
|
|
IReadOnlyDictionary<string, string> Headers,
|
|
string Body);
|
|
|
|
private sealed class ImmediateDelay : IRendezvousDelay
|
|
{
|
|
public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
}
|