154 lines
6.3 KiB
C#
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);
|
|
}
|
|
}
|