Files
Rendezvous/tests/FinalFactory.Rendezvous.Tests/Client/RendezvousCoordinatorIntegrationTests.cs
T
KyuubiYoru 94aba8a3bb
quality-gate / quality (push) Successful in 59s
feat(client): standardize connection outcomes (#13)
2026-07-16 10:18:41 +02:00

284 lines
13 KiB
C#

using System.Net;
using FinalFactory.Rendezvous.Client;
using FinalFactory.Rendezvous.Contracts;
using FinalFactory.Rendezvous.Server.JoinAttempts;
using FinalFactory.Rendezvous.Server.State;
using FinalFactory.Rendezvous.Server.Transport;
using FinalFactory.Rendezvous.Tests.JoinAttempts;
using LiteNetLib;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
namespace FinalFactory.Rendezvous.Tests.Client;
public sealed class RendezvousCoordinatorIntegrationTests
{
[Fact]
public async Task CallerOwnedManagersCompleteAuthenticatedDirectConnectionAndRejectTicketReplay()
{
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(8));
using JoinAttemptFixture fixture = new();
(RegisterSessionResponse registration, _) = fixture.CreateHost();
CreateJoinAttemptResponse created = fixture.Create(registration.ListingId, "sdk-direct-connect");
HostJoinAttempt hostAttempt = fixture.Service.BrowseForHost(
registration.ListingId,
ContractLimits.ContractVersion,
registration.LeaseToken,
ContractLimits.BrowserPageMaxItems,
null).Value!.Items.Single(item => item.AttemptId == created.AttemptId);
NatMediationProcessor processor = new(
fixture.Sessions.Store,
fixture.Sessions.Capabilities,
fixture.Service);
using UdpMediatorService mediatorService = new(
Options.Create(new UdpMediatorOptions
{
ListenAddress = IPAddress.Loopback.ToString(),
Port = 0,
PollIntervalMilliseconds = 1,
}),
NullLogger<UdpMediatorService>.Instance,
processor);
await mediatorService.StartAsync(timeout.Token);
RendezvousNetListener hostEvents = new();
RendezvousNetListener clientEvents = new();
bool gameplayConnectionRequestHandled = false;
hostEvents.GameplayEvents.ConnectionRequestEvent += _ =>
gameplayConnectionRequestHandled = true;
EventBasedNatPunchListener hostPunch = hostEvents.PunchEvents;
EventBasedNatPunchListener clientPunch = clientEvents.PunchEvents;
NetManager hostManager = hostEvents.CreateManager();
NetManager clientManager = clientEvents.CreateManager();
string? introductionToken = null;
string? hostIntroductionToken = null;
clientPunch.NatIntroductionSuccess += (_, _, token) => introductionToken = token;
hostPunch.NatIntroductionSuccess += (_, _, token) => hostIntroductionToken = token;
try
{
Assert.True(hostManager.Start(0));
Assert.True(clientManager.Start(0));
IPEndPoint mediator = Assert.IsType<IPEndPoint>(mediatorService.LocalEndpoint);
FakeJoinClient joinClient = new([hostAttempt]);
FixedCoordinatorClock clock = new(fixture.Sessions.Clock.UtcNow);
using ConnectionTicketValidator tickets = new(1_024, clock);
using RendezvousHostCoordinator host = new(
hostManager,
hostEvents,
mediator,
new PublishedSession(registration),
joinClient,
FastOptions(),
clock,
tickets);
using RendezvousClientCoordinator client = new(
clientManager,
clientEvents,
mediator,
created,
FastOptions(),
clock);
List<RendezvousHostAttemptCompletedEventArgs> hostCompletions = [];
List<RendezvousConnectionCompletedEventArgs> clientCompletions = [];
host.AttemptCompleted += (_, completion) => hostCompletions.Add(completion);
client.Completed += (_, completion) => clientCompletions.Add(completion);
Assert.True((await host.RefreshJoinAttemptsAsync(timeout.Token)).IsSuccess);
while ((!client.IsCompleted || hostCompletions.Count == 0)
&& !timeout.IsCancellationRequested)
{
host.Poll();
client.Poll();
await Task.Delay(2);
}
Assert.True(
client.State == RendezvousConnectionState.Connected,
$"Client ended in {client.State}; host pending={host.PendingAttemptCount}; "
+ $"host completions={hostCompletions.Count}; introduction={introductionToken is not null}; "
+ $"host introduction={hostIntroductionToken is not null}; "
+ $"client digest={NatIntroductionTokenCodec.MatchesDigest(introductionToken, created.ConnectionTicketDigest)}; "
+ $"host digest={NatIntroductionTokenCodec.MatchesDigest(hostIntroductionToken, hostAttempt.ConnectionTicketDigest)}.");
Assert.NotNull(client.ConnectedPeer);
Assert.Equal(
RendezvousConnectionState.Connected,
Assert.Single(clientCompletions).State);
RendezvousHostAttemptCompletedEventArgs hostCompletion = Assert.Single(hostCompletions);
Assert.Equal(created.AttemptId, hostCompletion.AttemptId);
Assert.Equal(RendezvousConnectionState.Connected, hostCompletion.State);
Assert.NotNull(hostCompletion.Peer);
Assert.False(gameplayConnectionRequestHandled);
Assert.True(NatIntroductionTokenCodec.TryDecode(
introductionToken,
out NatIntroductionToken? introduction));
Assert.NotNull(introduction);
EventBasedNetListener replayEvents = new();
bool replayConnected = false;
replayEvents.PeerConnectedEvent += _ => replayConnected = true;
NetManager replayManager = new(replayEvents);
try
{
Assert.True(replayManager.Start(0));
replayManager.Connect(
new IPEndPoint(IPAddress.Loopback, hostManager.LocalPort),
DirectConnectionRequestCodec.Encode(
created.AttemptId,
introduction.ConnectionTicket));
DateTime replayDeadline = DateTime.UtcNow.AddSeconds(1);
while (DateTime.UtcNow < replayDeadline && !replayConnected)
{
host.Poll();
replayManager.PollEvents();
await Task.Delay(2, timeout.Token);
}
Assert.False(replayConnected);
Assert.False(gameplayConnectionRequestHandled);
Assert.Single(hostCompletions);
}
finally
{
replayManager.Stop();
}
}
finally
{
hostManager.Stop();
clientManager.Stop();
await mediatorService.StopAsync(CancellationToken.None);
}
}
[Fact]
public async Task HostDefersADirectRequestUntilTheMatchingNatIntroductionArrives()
{
using JoinAttemptFixture fixture = new();
(RegisterSessionResponse registration, _) = fixture.CreateHost();
CreateJoinAttemptResponse created = fixture.Create(registration.ListingId, "direct-before-nat");
HostJoinAttempt hostAttempt = fixture.Service.BrowseForHost(
registration.ListingId,
ContractLimits.ContractVersion,
registration.LeaseToken,
ContractLimits.BrowserPageMaxItems,
null).Value!.Items.Single(item => item.AttemptId == created.AttemptId);
IntroductionEndpoints introduction = fixture.Introduce(registration, created);
ConnectionTicketGrant grant = Assert.IsType<ConnectionTicketGrant>(
fixture.Service.IssueConnectionTicket(introduction.Attempt).Value);
RendezvousNetListener hostEvents = new();
EventBasedNatPunchListener hostPunch = hostEvents.PunchEvents;
EventBasedNetListener clientEvents = new();
bool clientConnected = false;
clientEvents.PeerConnectedEvent += _ => clientConnected = true;
NetManager hostManager = hostEvents.CreateManager();
NetManager clientManager = new(clientEvents);
try
{
Assert.True(hostManager.Start(0));
Assert.True(clientManager.Start(0));
FixedCoordinatorClock clock = new(fixture.Sessions.Clock.UtcNow);
FakeJoinClient joins = new([hostAttempt]);
using ConnectionTicketValidator tickets = new(16, clock);
using RendezvousHostCoordinator host = new(
hostManager,
hostEvents,
new IPEndPoint(IPAddress.Loopback, 65_000),
new PublishedSession(registration),
joins,
FastOptions(),
clock,
tickets);
List<RendezvousHostAttemptCompletedEventArgs> completions = [];
host.AttemptCompleted += (_, completion) => completions.Add(completion);
Assert.True((await host.RefreshJoinAttemptsAsync()).IsSuccess);
host.Poll();
bool observedDeferredRequest = false;
hostEvents.RendezvousConnectionRequest += _ =>
{
observedDeferredRequest = host.DeferredRequestCount == 1;
((INatPunchListener)hostPunch).OnNatIntroductionSuccess(
new IPEndPoint(IPAddress.Loopback, clientManager.LocalPort),
NatAddressType.External,
grant.Ticket);
};
clientManager.Connect(
new IPEndPoint(IPAddress.Loopback, hostManager.LocalPort),
DirectConnectionRequestCodec.Encode(created.AttemptId, grant.Ticket));
DateTime connectedDeadline = DateTime.UtcNow.AddSeconds(1);
while ((!clientConnected || completions.Count == 0)
&& DateTime.UtcNow < connectedDeadline)
{
host.Poll();
clientManager.PollEvents();
await Task.Delay(2);
}
Assert.True(
clientConnected,
$"Deferred observed={observedDeferredRequest}; deferred={host.DeferredRequestCount}; "
+ $"pending={host.PendingAttemptCount}; completions={completions.Count}.");
Assert.True(observedDeferredRequest);
Assert.Equal(0, host.DeferredRequestCount);
Assert.Equal(
RendezvousConnectionState.Connected,
Assert.Single(completions).State);
}
finally
{
hostManager.Stop();
clientManager.Stop();
}
}
private static RendezvousCoordinatorOptions FastOptions() => new()
{
MaximumPunchRequests = 20,
InitialPunchRetryDelay = TimeSpan.FromMilliseconds(10),
MaximumPunchRetryDelay = TimeSpan.FromMilliseconds(100),
JitterRatio = 0,
};
private sealed class FakeJoinClient(IReadOnlyList<HostJoinAttempt> attempts) : IRendezvousJoinClient
{
public Task<RendezvousConnectionStartResult> CreateConnectionAttemptAsync(
CreateJoinAttemptRequest request,
NetworkEndpoint? dedicatedFallback = null,
CancellationToken cancellationToken = default) => throw new NotSupportedException();
public Task<RendezvousClientResult<CreateJoinAttemptResponse>> CreateAsync(
CreateJoinAttemptRequest request,
CancellationToken cancellationToken = default) => throw new NotSupportedException();
public Task<RendezvousClientResult<bool>> CancelAsync(
CreateJoinAttemptResponse attempt,
CancellationToken cancellationToken = default) => throw new NotSupportedException();
public Task<RendezvousClientResult<BrowseHostJoinAttemptsResponse>> BrowseForHostAsync(
PublishedSession session,
int pageSize = ContractLimits.BrowserPageMaxItems,
string? cursor = null,
CancellationToken cancellationToken = default) => throw new NotSupportedException();
public Task<RendezvousClientResult<IReadOnlyList<HostJoinAttempt>>> BrowseAllForHostAsync(
PublishedSession session,
int maximumPages = 100,
CancellationToken cancellationToken = default) => Task.FromResult(
RendezvousClientResult.Success(attempts));
public Task<RendezvousClientResult<ReportConnectionOutcomeResponse>> ReportOutcomeAsync(
CreateJoinAttemptResponse attempt,
RendezvousConnectionOutcome outcome,
CancellationToken cancellationToken = default) => throw new NotSupportedException();
}
private sealed class FixedCoordinatorClock(DateTimeOffset now) :
IRendezvousCoordinatorClock,
IConnectionTicketClock
{
public DateTimeOffset UtcNow { get; } = now;
public TimeSpan Elapsed => TimeSpan.Zero;
}
}