using FinalFactory.Rendezvous.Contracts; using FinalFactory.Rendezvous.Server.JoinAttempts; using FinalFactory.Rendezvous.Server.State; namespace FinalFactory.Rendezvous.Tests.JoinAttempts; public sealed class JoinAttemptServiceTests { [Fact] public void CreateIsIdempotentAndScopesDistinctRoleCredentials() { using JoinAttemptFixture fixture = new(); (RegisterSessionResponse registration, _) = fixture.CreateHost(); CreateJoinAttemptRequest request = fixture.Request(registration.ListingId, "stable-join-key"); JoinAttemptServiceResult first = fixture.Service.Create( fixture.ClientSubject, request); JoinAttemptServiceResult replay = fixture.Service.Create( fixture.ClientSubject, request); Assert.True(first.Succeeded); Assert.True(replay.Succeeded); Assert.Equal(first.Value!.AttemptId, replay.Value!.AttemptId); Assert.Equal(first.Value.MediationHandle, replay.Value.MediationHandle); Assert.Equal(first.Value.ClientPunchCapability, replay.Value.ClientPunchCapability); Assert.True(ContractValidation.IsCapabilityValid(first.Value.ClientPunchCapability)); Assert.InRange( first.Value.ClientPunchCapability.Length, 1, ContractLimits.LiteNetLibNatTokenMaxCharacters); HostJoinAttempt host = Assert.Single(fixture.Service.BrowseForHost( registration.ListingId, ContractLimits.ContractVersion, registration.LeaseToken, 10, null).Value!.Items); Assert.NotEqual(host.HostPunchCapability, first.Value.ClientPunchCapability); Assert.DoesNotContain(first.Value.ClientPunchCapability, fixture.Sessions.Store.ToString(), StringComparison.Ordinal); } [Fact] public void SameIdempotencyKeyWithDifferentRequestConflicts() { using JoinAttemptFixture fixture = new(); (RegisterSessionResponse registration, _) = fixture.CreateHost(); (RegisterSessionResponse other, _) = fixture.CreateHost(); CreateJoinAttemptRequest request = fixture.Request(registration.ListingId, "reused-key"); Assert.True(fixture.Service.Create(fixture.ClientSubject, request).Succeeded); request.ListingId = other.ListingId; JoinAttemptServiceResult conflict = fixture.Service.Create( fixture.ClientSubject, request); Assert.Equal(RendezvousErrorCode.Conflict, conflict.Error); } [Fact] public void CreationRejectsStaleIncompatibleAndCrossTenantListings() { using JoinAttemptFixture fixture = new(); (RegisterSessionResponse stale, _) = fixture.CreateHost(bindPresence: false); Assert.Equal( RendezvousErrorCode.NotFound, fixture.Service.Create(fixture.ClientSubject, fixture.Request(stale.ListingId)).Error); (RegisterSessionResponse active, _) = fixture.CreateHost(); CreateJoinAttemptRequest incompatible = fixture.Request(active.ListingId); incompatible.ProtocolVersion = 8; Assert.Equal( RendezvousErrorCode.IncompatibleProtocol, fixture.Service.Create(fixture.ClientSubject, incompatible).Error); CreateJoinAttemptRequest otherTenant = fixture.Request(active.ListingId); otherTenant.GameId = new("other-game"); Assert.Equal( RendezvousErrorCode.NotFound, fixture.Service.Create(fixture.ClientSubject, otherTenant).Error); } [Fact] public void HostPollingAuthenticatesLeaseAndUsesScopeBoundCursorPaging() { using JoinAttemptFixture fixture = new(); (RegisterSessionResponse registration, _) = fixture.CreateHost(); fixture.Create(registration.ListingId); fixture.Create(registration.ListingId); fixture.Create(registration.ListingId); JoinAttemptServiceResult first = fixture.Service.BrowseForHost( registration.ListingId, ContractLimits.ContractVersion, registration.LeaseToken, 1, null); Assert.True(first.Succeeded); Assert.Single(first.Value!.Items); Assert.NotNull(first.Value.NextCursor); JoinAttemptServiceResult second = fixture.Service.BrowseForHost( registration.ListingId, ContractLimits.ContractVersion, registration.LeaseToken, 1, first.Value.NextCursor); Assert.True(second.Succeeded); Assert.NotEqual(first.Value.Items[0].AttemptId, second.Value!.Items[0].AttemptId); Assert.Equal( RendezvousErrorCode.NotFound, fixture.Service.BrowseForHost( registration.ListingId, ContractLimits.ContractVersion, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 1, null).Error); Assert.Equal( RendezvousErrorCode.InvalidRequest, fixture.Service.BrowseForHost( registration.ListingId, ContractLimits.ContractVersion, registration.LeaseToken, 1, first.Value.NextCursor + "x").Error); (RegisterSessionResponse other, _) = fixture.CreateHost(); Assert.Equal( RendezvousErrorCode.InvalidRequest, fixture.Service.BrowseForHost( other.ListingId, ContractLimits.ContractVersion, other.LeaseToken, 1, first.Value.NextCursor).Error); } [Fact] public void CancellationRequiresTheAttemptsClientCapabilityAndRevokesState() { using JoinAttemptFixture fixture = new(); (RegisterSessionResponse registration, _) = fixture.CreateHost(); CreateJoinAttemptResponse created = fixture.Create(registration.ListingId); Assert.Equal( RendezvousErrorCode.NotFound, fixture.Service.Cancel( created.AttemptId, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").Error); Assert.True(fixture.Service.Cancel( created.AttemptId, created.ClientPunchCapability).Succeeded); Assert.Empty(fixture.Service.BrowseForHost( registration.ListingId, ContractLimits.ContractVersion, registration.LeaseToken, 10, null).Value!.Items); } [Fact] public void RoleAndAttemptCapabilitiesCannotCrossWireConcurrentAttempts() { using JoinAttemptFixture fixture = new(); (RegisterSessionResponse registration, _) = fixture.CreateHost(); CreateJoinAttemptResponse first = fixture.Create(registration.ListingId); CreateJoinAttemptResponse second = fixture.Create(registration.ListingId); StoredJoinAttempt firstStored = fixture.GetAttempt(registration, first.AttemptId); Assert.True(fixture.Sessions.Capabilities.TryFingerprint( second.ClientPunchCapability, out SecretFingerprint secondClientFingerprint)); Assert.True(fixture.Sessions.Capabilities.TryFingerprint( first.ClientPunchCapability, out SecretFingerprint firstClientFingerprint)); Assert.Equal( StoreResultCode.NotFound, fixture.Sessions.Store.BindAttemptEndpoint(new( firstStored.MediationHandle, AttemptPeerRole.Client, secondClientFingerprint, new(AddressFamilyKind.Ipv4, "198.51.100.20", 42_000), null)).Code); Assert.Equal( StoreResultCode.NotFound, fixture.Sessions.Store.BindAttemptEndpoint(new( firstStored.MediationHandle, AttemptPeerRole.Host, firstClientFingerprint, new(AddressFamilyKind.Ipv4, "203.0.113.20", 41_000), null)).Code); } [Fact] public async Task ConnectionTicketIsDistinctExpiringAndAtomicallySingleUse() { using JoinAttemptFixture fixture = new(); (RegisterSessionResponse registration, _) = fixture.CreateHost(); CreateJoinAttemptResponse created = fixture.Create(registration.ListingId); IntroductionEndpoints introduction = fixture.Introduce(registration, created); JoinAttemptServiceResult issued = fixture.Service.IssueConnectionTicket( introduction.Attempt); Assert.True(issued.Succeeded); Assert.True(ContractValidation.IsConnectionTicketValid(issued.Value!.Ticket)); Assert.NotEqual(created.ClientPunchCapability, issued.Value.Ticket); Assert.DoesNotContain(issued.Value.Ticket, issued.Value.ToString(), StringComparison.Ordinal); Assert.True(fixture.Sessions.Capabilities.TryFingerprint( issued.Value.Ticket, out SecretFingerprint ticketFingerprint)); ConsumeConnectionTicketCommand command = new(created.AttemptId, ticketFingerprint); using ManualResetEventSlim start = new(false); Task> left = Task.Run(() => { start.Wait(); return fixture.Sessions.Store.ConsumeConnectionTicket(command); }); Task> right = Task.Run(() => { start.Wait(); return fixture.Sessions.Store.ConsumeConnectionTicket(command); }); start.Set(); StoreResult[] results = await Task.WhenAll(left, right); Assert.Single(results, static result => result.Succeeded); Assert.Single(results, static result => result.Code == StoreResultCode.ReplayRejected); } [Fact] public void TicketRejectsAlteredCrossAttemptPreIntroductionAndExpiry() { EphemeralStoreOptions options = new() { ConnectionTicketLifetime = TimeSpan.FromSeconds(5), }; using JoinAttemptFixture fixture = new(options); (RegisterSessionResponse registration, _) = fixture.CreateHost(); CreateJoinAttemptResponse first = fixture.Create(registration.ListingId); CreateJoinAttemptResponse second = fixture.Create(registration.ListingId); StoredJoinAttempt firstStored = fixture.GetAttempt(registration, first.AttemptId); StoredJoinAttempt secondStored = fixture.GetAttempt(registration, second.AttemptId); Assert.Equal( StoreResultCode.Conflict, fixture.Sessions.Store.ConsumeConnectionTicket(new( first.AttemptId, firstStored.ConnectionTicketFingerprint)).Code); Assert.Equal( StoreResultCode.NotFound, fixture.Sessions.Store.ConsumeConnectionTicket(new( second.AttemptId, firstStored.ConnectionTicketFingerprint)).Code); fixture.Introduce(registration, first); fixture.Sessions.Clock.Advance(options.ConnectionTicketLifetime); Assert.Equal( StoreResultCode.Expired, fixture.Sessions.Store.ConsumeConnectionTicket(new( first.AttemptId, firstStored.ConnectionTicketFingerprint)).Code); Assert.False(secondStored.ConnectionTicketConsumed); } [Fact] public void TicketWindowBeginsAtIntroductionAndNeverOutlivesTheAttempt() { using JoinAttemptFixture fixture = new(); (RegisterSessionResponse registration, _) = fixture.CreateHost(); CreateJoinAttemptResponse created = fixture.Create(registration.ListingId); fixture.Sessions.Clock.Advance(TimeSpan.FromSeconds(15)); IntroductionEndpoints introduction = fixture.Introduce(registration, created); ConnectionTicketGrant ticket = Assert.IsType( fixture.Service.IssueConnectionTicket(introduction.Attempt).Value); Assert.Equal(created.ExpiresAt, ticket.ExpiresAt); Assert.Equal(TimeSpan.FromSeconds(15), ticket.ExpiresAt - fixture.Sessions.Clock.UtcNow); Assert.DoesNotContain( introduction.Attempt.CapabilityDerivationSalt, introduction.Attempt.ToString(), StringComparison.Ordinal); } }