Files
Rendezvous/tests/FinalFactory.Rendezvous.Tests/Browser/SessionBrowserServiceTests.cs
T
KyuubiYoru a9a2b3db35
quality-gate / quality (push) Successful in 57s
feat: add bounded compatible session browser (#8)
Closes #8
2026-07-16 06:06:29 +02:00

154 lines
6.3 KiB
C#

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