feat: add bounded compatible session browser (#8)
quality-gate / quality (push) Successful in 57s

Closes #8
This commit is contained in:
KyuubiYoru
2026-07-16 06:06:29 +02:00
parent 49564c7e7e
commit a9a2b3db35
14 changed files with 786 additions and 10 deletions
@@ -0,0 +1,153 @@
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<BrowseSessionsResponse> 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<SessionListingId> 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<string, string> metadata = Enumerable.Range(0, 14).ToDictionary(
static index => $"key-{index}",
static index => new string((char)('a' + index % 26), 256),
EqualityComparer<string>.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<string, string>(StringComparer.Ordinal)
{
["mode"] = "co-op",
["map"] = "<script>alert(1)</script>",
});
BrowseSessionsResponse response = fixture.Browser.Browse(fixture.Request()).Value!;
string json = JsonSerializer.Serialize(response, ContractJson.Options);
Assert.DoesNotContain("<script>", json, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("endpoint", json, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("token", json, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("capability", json, StringComparison.OrdinalIgnoreCase);
Assert.Equal("<script>alert(1)</script>", 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);
}
}
@@ -0,0 +1,71 @@
using FinalFactory.Rendezvous.Contracts;
using FinalFactory.Rendezvous.Server.Browser;
using FinalFactory.Rendezvous.Server.State;
using FinalFactory.Rendezvous.Tests.State;
namespace FinalFactory.Rendezvous.Tests.Browser;
internal sealed class SessionBrowserFixture : IDisposable
{
private readonly EphemeralStateFixture _state = new();
public SessionBrowserFixture()
{
Cursors = new();
Browser = new(_state.Store, Cursors, _state.Clock);
}
public InMemoryEphemeralRendezvousStore Store => _state.Store;
public ManualRendezvousClock Clock => _state.Clock;
public SessionBrowserCursorCodec Cursors { get; }
public SessionBrowserService Browser { get; }
public TenantScope Scope => _state.Scope;
public StoredListing Add(
TenantScope? scope = null,
uint protocolVersion = 7,
RegionId? regionId = null,
ListingVisibility visibility = ListingVisibility.Public,
bool fresh = true,
int currentPlayers = 1,
int maximumPlayers = 8,
IReadOnlyDictionary<string, string>? metadata = null)
{
CreateListingCommand seed = _state.ListingCommand();
CreateListingCommand command = seed with
{
Listing = seed.Listing with
{
Scope = scope ?? Scope,
ProtocolVersion = protocolVersion,
RegionId = regionId ?? seed.Listing.RegionId,
Visibility = visibility,
CurrentPlayers = currentPlayers,
MaximumPlayers = maximumPlayers,
Metadata = metadata ?? seed.Listing.Metadata,
},
};
StoredListing listing = Store.CreateListing(command).Value!;
if (fresh)
{
listing = Store.BindHostPresence(new(
command.Listing.HostPresenceHandle,
command.Listing.HostPresenceFingerprint,
EphemeralStateFixture.PublicEndpoint(40_000),
null)).Value!;
}
return listing;
}
public BrowseSessionsRequest Request(int pageSize = 100) => new()
{
GameId = Scope.GameId,
EnvironmentId = Scope.EnvironmentId,
ProtocolVersion = 7,
RegionId = new("eu-central"),
PageSize = pageSize,
};
public void Dispose() => Cursors.Dispose();
}