Files
Rendezvous/tests/FinalFactory.Rendezvous.Tests/Client/RendezvousJoinClientTests.cs
T
KyuubiYoru b4b6072fe1
quality-gate / quality (push) Successful in 56s
feat(client): add rendezvous traversal coordinators (#12)
2026-07-16 08:39:05 +02:00

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;
}
}
}