using System.Text.Json; using FinalFactory.Rendezvous.Contracts; using FinalFactory.Rendezvous.Server.Browser; using FinalFactory.Rendezvous.Server.State; namespace FinalFactory.Rendezvous.Tests.Browser; public sealed class SessionBrowserServiceTests { [Fact] public void ListEnforcesTenantProtocolPresenceVisibilityAndAvailabilityFilters() { using SessionBrowserFixture fixture = new(); StoredListing eligible = fixture.Add(); fixture.Add(scope: new(new("other-game"), fixture.Scope.EnvironmentId)); fixture.Add(scope: new(fixture.Scope.GameId, new("other-env"))); fixture.Add(protocolVersion: 8); fixture.Add(regionId: new("us-east")); fixture.Add(visibility: ListingVisibility.Unlisted); fixture.Add(fresh: false); fixture.Add(currentPlayers: 8, maximumPlayers: 8); BrowseSessionsRequest request = fixture.Request(); request.ExcludeFull = true; BrowserServiceResult result = fixture.Browser.Browse(request); Assert.True(result.Succeeded); Assert.Collection(result.Value!.Items, item => Assert.Equal(eligible.Definition.ListingId, item.ListingId)); } [Fact] public void UnguessableIdRetrievalAllowsFreshUnlistedOnlyWithinExactScope() { using SessionBrowserFixture fixture = new(); StoredListing unlisted = fixture.Add(visibility: ListingVisibility.Unlisted); Assert.True(fixture.Browser.Get( unlisted.Definition.ListingId, fixture.Scope.GameId, fixture.Scope.EnvironmentId, 7).Succeeded); Assert.Equal(RendezvousErrorCode.NotFound, fixture.Browser.Get( unlisted.Definition.ListingId, new("other-game"), fixture.Scope.EnvironmentId, 7).Error); Assert.Equal(RendezvousErrorCode.NotFound, fixture.Browser.Get( unlisted.Definition.ListingId, fixture.Scope.GameId, fixture.Scope.EnvironmentId, 8).Error); fixture.Clock.Advance(TimeSpan.FromSeconds(20)); Assert.Equal(RendezvousErrorCode.NotFound, fixture.Browser.Get( unlisted.Definition.ListingId, fixture.Scope.GameId, fixture.Scope.EnvironmentId, 7).Error); } [Fact] public void KeysetCursorReturnsStableRecordsOnceAndRejectsTamperingOrRescoping() { using SessionBrowserFixture fixture = new(); for (int index = 0; index < 7; index++) { fixture.Add(); } BrowseSessionsRequest request = fixture.Request(pageSize: 2); List seen = []; do { BrowseSessionsResponse page = fixture.Browser.Browse(request).Value!; seen.AddRange(page.Items.Select(static item => item.ListingId)); request.Cursor = page.NextCursor; } while (request.Cursor is not null); Assert.Equal(7, seen.Count); Assert.Equal(7, seen.Distinct().Count()); Assert.Equal(seen.OrderBy(static id => id.Value), seen); BrowseSessionsRequest tampered = fixture.Request(pageSize: 2); tampered.Cursor = fixture.Browser.Browse(tampered).Value!.NextCursor + "A"; Assert.Equal(RendezvousErrorCode.InvalidRequest, fixture.Browser.Browse(tampered).Error); BrowseSessionsRequest rescoped = fixture.Request(pageSize: 2); rescoped.Cursor = fixture.Browser.Browse(fixture.Request(pageSize: 2)).Value!.NextCursor; rescoped.ExcludeFull = true; Assert.Equal(RendezvousErrorCode.InvalidRequest, fixture.Browser.Browse(rescoped).Error); BrowseSessionsRequest expired = fixture.Request(pageSize: 2); expired.Cursor = fixture.Browser.Browse(expired).Value!.NextCursor; fixture.Clock.Advance(TimeSpan.FromMinutes(5)); Assert.Equal(RendezvousErrorCode.InvalidRequest, fixture.Browser.Browse(expired).Error); } [Fact] public void ResponseByteBudgetTrimsLargePagesAndContinuesWithCursor() { using SessionBrowserFixture fixture = new(); Dictionary metadata = Enumerable.Range(0, 14).ToDictionary( static index => $"key-{index}", static index => new string((char)('a' + index % 26), 256), EqualityComparer.Default); for (int index = 0; index < 100; index++) { fixture.Add(metadata: metadata); } BrowseSessionsResponse response = fixture.Browser.Browse(fixture.Request()).Value!; int encodedBytes = JsonSerializer.SerializeToUtf8Bytes(response, ContractJson.Options).Length; Assert.InRange(encodedBytes, 1, ContractLimits.BrowserResponseMaxBytes); Assert.NotEmpty(response.Items); Assert.NotNull(response.NextCursor); Assert.True(response.Items.Count < 100); } [Fact] public void PresentationMetadataIsJsonEscapedAndResponseHasNoConnectionSecrets() { using SessionBrowserFixture fixture = new(); fixture.Add(metadata: new Dictionary(StringComparer.Ordinal) { ["mode"] = "co-op", ["map"] = "", }); BrowseSessionsResponse response = fixture.Browser.Browse(fixture.Request()).Value!; string json = JsonSerializer.Serialize(response, ContractJson.Options); Assert.DoesNotContain("", Assert.Single(response.Items).Metadata["map"]); } [Fact] public void RevokedListingDisappearsBeforeAnotherReadPathCanObserveIt() { using SessionBrowserFixture fixture = new(); StoredListing listing = fixture.Add(); fixture.Store.RevokeListing(listing.Definition.ListingId); Assert.Empty(fixture.Browser.Browse(fixture.Request()).Value!.Items); Assert.Equal(RendezvousErrorCode.NotFound, fixture.Browser.Get( listing.Definition.ListingId, fixture.Scope.GameId, fixture.Scope.EnvironmentId, 7).Error); } }