feat: add presence-gated session leases (#7)
quality-gate / quality (push) Successful in 55s
quality-gate / quality (push) Successful in 55s
Closes #7
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
using System.Text.Json;
|
||||
using FinalFactory.Rendezvous.Contracts;
|
||||
using FinalFactory.Rendezvous.Server.Provisioning;
|
||||
using FinalFactory.Rendezvous.Server.Sessions;
|
||||
using FinalFactory.Rendezvous.Server.State;
|
||||
|
||||
namespace FinalFactory.Rendezvous.Tests.Sessions;
|
||||
|
||||
public sealed class SessionLeaseServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void RegistrationReturnsOpaqueCredentialsButRemainsHiddenUntilPresence()
|
||||
{
|
||||
using SessionLeaseFixture fixture = new();
|
||||
|
||||
RegisterSessionResponse response = fixture.Register();
|
||||
|
||||
Assert.Equal(30, response.LeaseRenewAfterSeconds);
|
||||
Assert.Equal(10, response.HostPresenceRefreshAfterSeconds);
|
||||
Assert.Equal(43, response.LeaseToken.Length);
|
||||
Assert.Equal(43, response.HostPresenceCapability.Length);
|
||||
Assert.Empty(fixture.Browse());
|
||||
string json = JsonSerializer.Serialize(response, ContractJson.Options);
|
||||
Assert.DoesNotContain("endpoint", json, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("fingerprint", json, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("store", json, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExactRegistrationRetryReproducesIdsAndCapabilitiesWithoutRetainingPlaintext()
|
||||
{
|
||||
using SessionLeaseFixture fixture = new();
|
||||
RegisterSessionRequest request = fixture.Request("same-key");
|
||||
|
||||
RegisterSessionResponse first = fixture.Register(request);
|
||||
RegisterSessionRequest reordered = fixture.Request("same-key");
|
||||
reordered.Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["map"] = "europa",
|
||||
["mode"] = "co-op",
|
||||
};
|
||||
RegisterSessionResponse duplicate = fixture.Register(reordered);
|
||||
RegisterSessionRequest changedRequest = fixture.Request("same-key");
|
||||
changedRequest.DisplayName = "Changed";
|
||||
SessionServiceResult<RegisterSessionResponse> changed = fixture.Service.Register(
|
||||
fixture.Principal,
|
||||
changedRequest);
|
||||
|
||||
Assert.Equal(first.ListingId, duplicate.ListingId);
|
||||
Assert.Equal(first.LeaseId, duplicate.LeaseId);
|
||||
Assert.Equal(first.LeaseToken, duplicate.LeaseToken);
|
||||
Assert.Equal(first.HostPresenceHandle, duplicate.HostPresenceHandle);
|
||||
Assert.Equal(first.HostPresenceCapability, duplicate.HostPresenceCapability);
|
||||
Assert.Equal(RendezvousErrorCode.Conflict, changed.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReRegistrationAfterIdempotencyExpiryRotatesIdsAndCapabilities()
|
||||
{
|
||||
EphemeralStoreOptions options = new()
|
||||
{
|
||||
LeaseLifetime = TimeSpan.FromSeconds(5),
|
||||
JoinAttemptLifetime = TimeSpan.FromSeconds(5),
|
||||
IdempotencyLifetime = TimeSpan.FromSeconds(6),
|
||||
};
|
||||
using SessionLeaseFixture fixture = new(options);
|
||||
RegisterSessionRequest request = fixture.Request("reused-after-expiry");
|
||||
RegisterSessionResponse first = fixture.Register(request);
|
||||
|
||||
fixture.Clock.Advance(options.IdempotencyLifetime);
|
||||
RegisterSessionResponse second = fixture.Register(request);
|
||||
|
||||
Assert.NotEqual(first.ListingId, second.ListingId);
|
||||
Assert.NotEqual(first.LeaseId, second.LeaseId);
|
||||
Assert.NotEqual(first.LeaseToken, second.LeaseToken);
|
||||
Assert.NotEqual(first.HostPresenceCapability, second.HostPresenceCapability);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PresenceTransitionsAwaitingToListedToStaleAndBackWithoutChangingIdentity()
|
||||
{
|
||||
using SessionLeaseFixture fixture = new();
|
||||
RegisterSessionResponse registration = fixture.Register();
|
||||
|
||||
Assert.Empty(fixture.Browse());
|
||||
Assert.True(fixture.BindPresence(registration).Succeeded);
|
||||
Assert.Equal(registration.ListingId, Assert.Single(fixture.Browse()).Definition.ListingId);
|
||||
|
||||
fixture.Clock.Advance(fixture.StoreOptions.PresenceLifetime);
|
||||
Assert.Empty(fixture.Browse());
|
||||
Assert.True(fixture.BindPresence(registration).Succeeded);
|
||||
Assert.Equal(registration.ListingId, Assert.Single(fixture.Browse()).Definition.ListingId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenewUpdateAndDeleteMaintainCanonicalIdentityAndAdvisoryCapacity()
|
||||
{
|
||||
using SessionLeaseFixture fixture = new();
|
||||
RegisterSessionResponse registration = fixture.Register();
|
||||
fixture.Clock.Advance(TimeSpan.FromSeconds(1));
|
||||
|
||||
SessionServiceResult<RenewLeaseResponse> renewed = fixture.Service.Renew(
|
||||
fixture.Principal,
|
||||
registration.ListingId,
|
||||
new() { LeaseToken = registration.LeaseToken });
|
||||
SessionServiceResult<bool> updated = fixture.Service.Update(
|
||||
fixture.Principal,
|
||||
registration.ListingId,
|
||||
new()
|
||||
{
|
||||
LeaseToken = registration.LeaseToken,
|
||||
BuildVersion = "1.4.3",
|
||||
DisplayName = "Europa Updated",
|
||||
Capacity = new() { CurrentPlayers = 8, MaximumPlayers = 8 },
|
||||
Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["mode"] = "co-op",
|
||||
["map"] = "europa",
|
||||
},
|
||||
});
|
||||
StoredListing stored = fixture.Store.GetListing(registration.ListingId, false).Value!;
|
||||
|
||||
Assert.True(renewed.Succeeded);
|
||||
Assert.Equal(fixture.Clock.UtcNow.Add(fixture.StoreOptions.LeaseLifetime), renewed.Value!.ExpiresAt);
|
||||
Assert.True(updated.Succeeded);
|
||||
Assert.Equal(registration.ListingId, stored.Definition.ListingId);
|
||||
Assert.Equal(fixture.Scope, stored.Definition.Scope);
|
||||
Assert.Equal(8, stored.Definition.CurrentPlayers);
|
||||
Assert.Equal(8, stored.Definition.MaximumPlayers);
|
||||
Assert.True(fixture.Service.Delete(
|
||||
fixture.Principal,
|
||||
registration.ListingId,
|
||||
new() { LeaseToken = registration.LeaseToken }).Succeeded);
|
||||
Assert.True(fixture.Service.Delete(
|
||||
fixture.Principal,
|
||||
registration.ListingId,
|
||||
new() { LeaseToken = registration.LeaseToken }).Succeeded);
|
||||
Assert.Equal(StoreResultCode.NotFound, fixture.Store.GetListing(registration.ListingId, false).Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnotherPublisherCannotRenewUpdateOrDeleteListing()
|
||||
{
|
||||
using SessionLeaseFixture fixture = new();
|
||||
RegisterSessionResponse registration = fixture.Register();
|
||||
DedicatedPublisherPrincipal other = fixture.Publisher("publisher-2");
|
||||
|
||||
Assert.Equal(RendezvousErrorCode.NotFound, fixture.Service.Renew(
|
||||
other,
|
||||
registration.ListingId,
|
||||
new() { LeaseToken = registration.LeaseToken }).Error);
|
||||
Assert.Equal(RendezvousErrorCode.NotFound, fixture.Service.Update(
|
||||
other,
|
||||
registration.ListingId,
|
||||
new()
|
||||
{
|
||||
LeaseToken = registration.LeaseToken,
|
||||
BuildVersion = "1.4.3",
|
||||
DisplayName = "Hijacked",
|
||||
Capacity = new() { CurrentPlayers = 1, MaximumPlayers = 2 },
|
||||
Metadata = new Dictionary<string, string> { ["mode"] = "co-op" },
|
||||
}).Error);
|
||||
Assert.True(fixture.Service.Delete(
|
||||
other,
|
||||
registration.ListingId,
|
||||
new() { LeaseToken = registration.LeaseToken }).Succeeded);
|
||||
Assert.True(fixture.Store.GetListing(registration.ListingId, false).Succeeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConcurrentRenewDeleteCannotResurrectListing()
|
||||
{
|
||||
using SessionLeaseFixture fixture = new();
|
||||
RegisterSessionResponse registration = fixture.Register();
|
||||
using ManualResetEventSlim start = new(false);
|
||||
Task<SessionServiceResult<RenewLeaseResponse>> renew = Task.Run(() =>
|
||||
{
|
||||
start.Wait();
|
||||
return fixture.Service.Renew(
|
||||
fixture.Principal,
|
||||
registration.ListingId,
|
||||
new() { LeaseToken = registration.LeaseToken });
|
||||
});
|
||||
Task<SessionServiceResult<bool>> delete = Task.Run(() =>
|
||||
{
|
||||
start.Wait();
|
||||
return fixture.Service.Delete(
|
||||
fixture.Principal,
|
||||
registration.ListingId,
|
||||
new() { LeaseToken = registration.LeaseToken });
|
||||
});
|
||||
|
||||
start.Set();
|
||||
await Task.WhenAll(renew, delete);
|
||||
SessionServiceResult<RenewLeaseResponse> renewResult = await renew;
|
||||
SessionServiceResult<bool> deleteResult = await delete;
|
||||
|
||||
Assert.True(deleteResult.Succeeded);
|
||||
Assert.Contains(renewResult.Error, new[]
|
||||
{
|
||||
RendezvousErrorCode.None,
|
||||
RendezvousErrorCode.NotFound,
|
||||
RendezvousErrorCode.Conflict,
|
||||
});
|
||||
Assert.Equal(StoreResultCode.NotFound, fixture.Store.GetListing(registration.ListingId, false).Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AbandonedRegistrationExpiresAndFreesBoundedCapacity()
|
||||
{
|
||||
EphemeralStoreOptions options = new()
|
||||
{
|
||||
MaxListings = 1,
|
||||
LeaseLifetime = TimeSpan.FromSeconds(5),
|
||||
};
|
||||
using SessionLeaseFixture fixture = new(options);
|
||||
fixture.Register(fixture.Request("first"));
|
||||
Assert.Equal(RendezvousErrorCode.CapacityExceeded, fixture.Service.Register(
|
||||
fixture.Principal,
|
||||
fixture.Request("second")).Error);
|
||||
|
||||
fixture.Clock.Advance(options.LeaseLifetime);
|
||||
|
||||
Assert.True(fixture.Service.Register(
|
||||
fixture.Principal,
|
||||
fixture.Request("second")).Succeeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeaseExpiryRemovesMutationAndPresencePaths()
|
||||
{
|
||||
using SessionLeaseFixture fixture = new();
|
||||
RegisterSessionResponse registration = fixture.Register();
|
||||
fixture.BindPresence(registration);
|
||||
|
||||
fixture.Clock.Advance(fixture.StoreOptions.LeaseLifetime);
|
||||
|
||||
Assert.Empty(fixture.Browse());
|
||||
Assert.Equal(RendezvousErrorCode.NotFound, fixture.Service.Renew(
|
||||
fixture.Principal,
|
||||
registration.ListingId,
|
||||
new() { LeaseToken = registration.LeaseToken }).Error);
|
||||
Assert.Equal(StoreResultCode.NotFound, fixture.BindPresence(registration).Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LossOfAtomicStateFailsLeaseMutationClosed()
|
||||
{
|
||||
using SessionLeaseFixture fixture = new();
|
||||
RegisterSessionResponse registration = fixture.Register();
|
||||
fixture.Store.MarkUnavailable();
|
||||
|
||||
Assert.Equal(RendezvousErrorCode.ServiceUnavailable, fixture.Service.Renew(
|
||||
fixture.Principal,
|
||||
registration.ListingId,
|
||||
new() { LeaseToken = registration.LeaseToken }).Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidPolicyBoundInputsReturnStableTypedErrors()
|
||||
{
|
||||
using SessionLeaseFixture fixture = new();
|
||||
RegisterSessionRequest capacity = fixture.Request("bad-capacity");
|
||||
capacity.Capacity = new() { CurrentPlayers = 2, MaximumPlayers = 1 };
|
||||
Assert.Equal(RendezvousErrorCode.InvalidRequest, fixture.Service.Register(
|
||||
fixture.Principal,
|
||||
capacity).Error);
|
||||
RegisterSessionRequest protocol = fixture.Request("bad-protocol");
|
||||
protocol.ProtocolVersion = 8;
|
||||
Assert.Equal(RendezvousErrorCode.IncompatibleProtocol, fixture.Service.Register(
|
||||
fixture.Principal,
|
||||
protocol).Error);
|
||||
RegisterSessionRequest region = fixture.Request("bad-region");
|
||||
region.RegionId = new("us-east");
|
||||
Assert.Equal(RendezvousErrorCode.InvalidRequest, fixture.Service.Register(
|
||||
fixture.Principal,
|
||||
region).Error);
|
||||
RegisterSessionRequest visibility = fixture.Request("bad-visibility");
|
||||
visibility.Visibility = (ListingVisibility)99;
|
||||
Assert.Equal(RendezvousErrorCode.InvalidRequest, fixture.Service.Register(
|
||||
fixture.Principal,
|
||||
visibility).Error);
|
||||
RegisterSessionRequest metadata = fixture.Request("bad-metadata");
|
||||
metadata.Metadata = new Dictionary<string, string> { ["unknown"] = "value" };
|
||||
Assert.Equal(RendezvousErrorCode.InvalidRequest, fixture.Service.Register(
|
||||
fixture.Principal,
|
||||
metadata).Error);
|
||||
RegisterSessionRequest build = fixture.Request("bad-build");
|
||||
build.BuildVersion = " ";
|
||||
Assert.Equal(RendezvousErrorCode.InvalidRequest, fixture.Service.Register(
|
||||
fixture.Principal,
|
||||
build).Error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user