599 lines
27 KiB
C#
599 lines
27 KiB
C#
using FinalFactory.Rendezvous.Contracts;
|
|
using FinalFactory.Rendezvous.Server.State;
|
|
|
|
namespace FinalFactory.Rendezvous.Tests.State;
|
|
|
|
public sealed class InMemoryEphemeralRendezvousStoreTests
|
|
{
|
|
[Fact]
|
|
public void DeterministicHostileStateTransitionsStayTypedAndCapacityBounded()
|
|
{
|
|
const int seed = 0x15_57A7E;
|
|
Random random = new(seed);
|
|
EphemeralStateFixture fixture = new(new EphemeralStoreOptions
|
|
{
|
|
MaxJoinAttempts = 32,
|
|
});
|
|
StoredListing listing = fixture.CreateVisibleListing(out _);
|
|
|
|
for (int iteration = 0; iteration < 1_000; iteration++)
|
|
{
|
|
CreateJoinAttemptCommand command = fixture.AttemptCommand(listing);
|
|
command = random.Next(4) switch
|
|
{
|
|
0 => command with
|
|
{
|
|
Scope = new(new GameId("other-game"), new EnvironmentId("test")),
|
|
},
|
|
1 => command with { ProtocolVersion = command.ProtocolVersion + 1 },
|
|
_ => command,
|
|
};
|
|
StoreResult<StoredJoinAttempt> created = fixture.Store.CreateJoinAttempt(command);
|
|
Assert.True(Enum.IsDefined(created.Code));
|
|
if (!created.Succeeded || created.Value is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
SecretFingerprint supplied = random.Next(3) == 0
|
|
? EphemeralStateFixture.Fingerprint($"wrong-{iteration}")
|
|
: created.Value.ClientCapabilityFingerprint;
|
|
StoreResult<StoredJoinAttempt> bound = fixture.Store.BindAttemptEndpoint(new(
|
|
created.Value.MediationHandle,
|
|
AttemptPeerRole.Client,
|
|
supplied,
|
|
EphemeralStateFixture.OtherPublicEndpoint(20_000 + iteration),
|
|
null));
|
|
Assert.True(Enum.IsDefined(bound.Code));
|
|
}
|
|
|
|
StoreResult<IReadOnlyList<StoredJoinAttempt>> attempts =
|
|
fixture.Store.BrowseHostJoinAttempts(new(
|
|
listing.Definition.ListingId,
|
|
listing.Definition.LeaseFingerprint,
|
|
100));
|
|
Assert.True(attempts.Succeeded);
|
|
Assert.InRange(attempts.Value!.Count, 1, 32);
|
|
}
|
|
|
|
[Fact]
|
|
public void IdempotencyRetentionMustCoverResourceLifetimes()
|
|
{
|
|
ManualRendezvousClock clock = new();
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new InMemoryEphemeralRendezvousStore(
|
|
new EphemeralStoreOptions { IdempotencyLifetime = TimeSpan.FromSeconds(5) },
|
|
clock,
|
|
clock));
|
|
}
|
|
|
|
[Fact]
|
|
public void DuplicateRegistrationIsIdempotentButChangedRequestConflicts()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
CreateListingCommand command = fixture.ListingCommand();
|
|
|
|
StoreResult<StoredListing> first = fixture.Store.CreateListing(command);
|
|
StoreResult<StoredListing> duplicate = fixture.Store.CreateListing(command);
|
|
StoreResult<StoredListing> changed = fixture.Store.CreateListing(command with { RequestFingerprint = "different" });
|
|
|
|
Assert.True(first.Succeeded);
|
|
Assert.True(duplicate.Succeeded);
|
|
Assert.True(duplicate.IsIdempotentReplay);
|
|
Assert.Equal(first.Value!.Definition.ListingId, duplicate.Value!.Definition.ListingId);
|
|
Assert.Equal(StoreResultCode.Conflict, changed.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public void LeaseAndPresenceExpiryUseMonotonicTime()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
StoredListing listing = fixture.CreateVisibleListing(out _);
|
|
|
|
fixture.Clock.Advance(TimeSpan.FromSeconds(20));
|
|
Assert.Equal(StoreResultCode.NotFound, fixture.Store.GetListing(listing.Definition.ListingId, true).Code);
|
|
Assert.True(fixture.Store.GetListing(listing.Definition.ListingId, false).Succeeded);
|
|
|
|
fixture.Clock.Advance(TimeSpan.FromSeconds(40));
|
|
Assert.Equal(StoreResultCode.NotFound, fixture.Store.GetListing(listing.Definition.ListingId, false).Code);
|
|
}
|
|
|
|
[Fact]
|
|
public void WallClockMovementDoesNotExpireOrExtendLease()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
StoredListing listing = fixture.CreateVisibleListing(out CreateListingCommand command);
|
|
|
|
fixture.Clock.MoveWall(TimeSpan.FromDays(30));
|
|
Assert.True(fixture.Store.GetListing(listing.Definition.ListingId, false).Succeeded);
|
|
fixture.Clock.Advance(TimeSpan.FromSeconds(1));
|
|
StoreResult<StoredListing> renewed = fixture.Store.RenewLease(new(
|
|
listing.Definition.ListingId,
|
|
listing.Definition.LeaseId,
|
|
listing.Definition.LeaseFingerprint,
|
|
listing.Definition.OwnerSubject,
|
|
listing.Version));
|
|
Assert.Equal(new DateTimeOffset(2026, 7, 16, 0, 1, 1, TimeSpan.Zero), renewed.Value!.LeaseExpiresAt);
|
|
fixture.Clock.MoveWall(TimeSpan.FromDays(-60));
|
|
fixture.Clock.Advance(TimeSpan.FromSeconds(59));
|
|
Assert.True(fixture.Store.GetListing(listing.Definition.ListingId, false).Succeeded);
|
|
fixture.Clock.Advance(TimeSpan.FromSeconds(1));
|
|
Assert.Equal(StoreResultCode.NotFound, fixture.Store.GetListing(command.Listing.ListingId, false).Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RenewDeleteRaceIsAtomicAndDeleteAlwaysWinsEventually()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
StoredListing listing = fixture.CreateVisibleListing(out CreateListingCommand command);
|
|
using ManualResetEventSlim start = new(false);
|
|
|
|
Task<StoreResult<StoredListing>> renew = Task.Run(() =>
|
|
{
|
|
start.Wait();
|
|
return fixture.Store.RenewLease(new(
|
|
listing.Definition.ListingId,
|
|
listing.Definition.LeaseId,
|
|
listing.Definition.LeaseFingerprint,
|
|
listing.Definition.OwnerSubject,
|
|
listing.Version));
|
|
});
|
|
Task<StoreResult<bool>> delete = Task.Run(() =>
|
|
{
|
|
start.Wait();
|
|
return fixture.Store.DeleteListing(new(
|
|
command.Listing.ListingId,
|
|
command.Listing.LeaseId,
|
|
command.Listing.LeaseFingerprint,
|
|
command.Listing.OwnerSubject));
|
|
});
|
|
|
|
start.Set();
|
|
await Task.WhenAll(renew, delete);
|
|
StoreResult<StoredListing> renewResult = await renew;
|
|
StoreResult<bool> deleteResult = await delete;
|
|
|
|
Assert.True(deleteResult.Succeeded);
|
|
Assert.Contains(renewResult.Code, new[] { StoreResultCode.Success, StoreResultCode.NotFound });
|
|
Assert.Equal(StoreResultCode.NotFound, fixture.Store.GetListing(command.Listing.ListingId, false).Code);
|
|
}
|
|
|
|
[Fact]
|
|
public void CompareAndSwapPreventsStaleRenewal()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
StoredListing listing = fixture.CreateVisibleListing(out _);
|
|
RenewLeaseCommand command = new(
|
|
listing.Definition.ListingId,
|
|
listing.Definition.LeaseId,
|
|
listing.Definition.LeaseFingerprint,
|
|
listing.Definition.OwnerSubject,
|
|
listing.Version);
|
|
|
|
StoreResult<StoredListing> first = fixture.Store.RenewLease(command);
|
|
StoreResult<StoredListing> stale = fixture.Store.RenewLease(command);
|
|
|
|
Assert.Equal(2, first.Value!.Version);
|
|
Assert.Equal(StoreResultCode.Conflict, stale.Code);
|
|
Assert.Equal(2, stale.Value!.Version);
|
|
}
|
|
|
|
[Fact]
|
|
public void JoinRequiresExactScopeProtocolAndFreshHostPresence()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
CreateListingCommand listingCommand = fixture.ListingCommand();
|
|
StoredListing listing = fixture.Store.CreateListing(listingCommand).Value!;
|
|
CreateJoinAttemptCommand attempt = fixture.AttemptCommand(listing);
|
|
|
|
Assert.Equal(StoreResultCode.StaleHost, fixture.Store.CreateJoinAttempt(attempt).Code);
|
|
fixture.Store.BindHostPresence(new(
|
|
listingCommand.Listing.HostPresenceHandle,
|
|
listingCommand.Listing.HostPresenceFingerprint,
|
|
EphemeralStateFixture.PublicEndpoint(40_000),
|
|
null));
|
|
Assert.Equal(StoreResultCode.IncompatibleProtocol, fixture.Store.CreateJoinAttempt(attempt with { ProtocolVersion = 8 }).Code);
|
|
Assert.Equal(StoreResultCode.NotFound, fixture.Store.CreateJoinAttempt(attempt with
|
|
{
|
|
Scope = new(new("other-game"), new("test")),
|
|
}).Code);
|
|
Assert.True(fixture.Store.CreateJoinAttempt(attempt).Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void DuplicateJoinIsIdempotentAndDoesNotAllocateTwice()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
StoredListing listing = fixture.CreateVisibleListing(out _);
|
|
CreateJoinAttemptCommand command = fixture.AttemptCommand(listing);
|
|
|
|
StoreResult<StoredJoinAttempt> first = fixture.Store.CreateJoinAttempt(command);
|
|
StoreResult<StoredJoinAttempt> duplicate = fixture.Store.CreateJoinAttempt(command);
|
|
|
|
Assert.True(first.Succeeded);
|
|
Assert.True(duplicate.Succeeded);
|
|
Assert.True(duplicate.IsIdempotentReplay);
|
|
Assert.Equal(first.Value!.AttemptId, duplicate.Value!.AttemptId);
|
|
}
|
|
|
|
[Fact]
|
|
public void BrowseReturnsOnlyFreshPublicCompatibleListingsInStableOrder()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
StoredListing visible = fixture.CreateVisibleListing(out _);
|
|
CreateListingCommand staleCommand = fixture.ListingCommand();
|
|
fixture.Store.CreateListing(staleCommand);
|
|
CreateListingCommand unlistedCommand = fixture.ListingCommand();
|
|
unlistedCommand = unlistedCommand with
|
|
{
|
|
Listing = unlistedCommand.Listing with { Visibility = ListingVisibility.Unlisted },
|
|
};
|
|
fixture.Store.CreateListing(unlistedCommand);
|
|
fixture.Store.BindHostPresence(new(
|
|
unlistedCommand.Listing.HostPresenceHandle,
|
|
unlistedCommand.Listing.HostPresenceFingerprint,
|
|
EphemeralStateFixture.PublicEndpoint(40_099),
|
|
null));
|
|
|
|
StoreResult<IReadOnlyList<StoredListing>> result = fixture.Store.BrowseVisibleListings(new(
|
|
fixture.Scope,
|
|
visible.Definition.ProtocolVersion,
|
|
visible.Definition.RegionId));
|
|
|
|
Assert.True(result.Succeeded);
|
|
Assert.Collection(result.Value!, item => Assert.Equal(visible.Definition.ListingId, item.Definition.ListingId));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConcurrentEndpointBindingAcceptsOneCompleteEndpointOnly()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
StoredListing listing = fixture.CreateVisibleListing(out _);
|
|
CreateJoinAttemptCommand command = fixture.AttemptCommand(listing);
|
|
fixture.Store.CreateJoinAttempt(command);
|
|
BindAttemptEndpointCommand first = new(
|
|
command.MediationHandle,
|
|
AttemptPeerRole.Client,
|
|
command.ClientCapabilityFingerprint,
|
|
EphemeralStateFixture.PublicEndpoint(40_001),
|
|
EphemeralStateFixture.LocalEndpoint(40_001));
|
|
BindAttemptEndpointCommand second = first with
|
|
{
|
|
PublicEndpoint = EphemeralStateFixture.OtherPublicEndpoint(50_001),
|
|
LocalEndpoint = null,
|
|
};
|
|
using ManualResetEventSlim start = new(false);
|
|
|
|
Task<StoreResult<StoredJoinAttempt>> left = Task.Run(() => { start.Wait(); return fixture.Store.BindAttemptEndpoint(first); });
|
|
Task<StoreResult<StoredJoinAttempt>> right = Task.Run(() => { start.Wait(); return fixture.Store.BindAttemptEndpoint(second); });
|
|
start.Set();
|
|
await Task.WhenAll(left, right);
|
|
StoreResult<StoredJoinAttempt> leftResult = await left;
|
|
StoreResult<StoredJoinAttempt> rightResult = await right;
|
|
|
|
Assert.Equal(1, new[] { leftResult, rightResult }.Count(static result => result.Succeeded));
|
|
Assert.Equal(1, new[] { leftResult, rightResult }.Count(static result => result.Code == StoreResultCode.ReplayRejected));
|
|
}
|
|
|
|
[Fact]
|
|
public void AttemptCapabilitiesAndIntroductionAreOneTime()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
StoredListing listing = fixture.CreateVisibleListing(out _);
|
|
CreateJoinAttemptCommand command = fixture.AttemptCommand(listing);
|
|
fixture.Store.CreateJoinAttempt(command);
|
|
BindAttemptEndpointCommand host = new(
|
|
command.MediationHandle,
|
|
AttemptPeerRole.Host,
|
|
command.HostCapabilityFingerprint,
|
|
EphemeralStateFixture.PublicEndpoint(40_010),
|
|
null);
|
|
BindAttemptEndpointCommand client = new(
|
|
command.MediationHandle,
|
|
AttemptPeerRole.Client,
|
|
command.ClientCapabilityFingerprint,
|
|
EphemeralStateFixture.OtherPublicEndpoint(40_020),
|
|
null);
|
|
|
|
Assert.True(fixture.Store.BindAttemptEndpoint(host).Succeeded);
|
|
Assert.True(fixture.Store.BindAttemptEndpoint(client).Succeeded);
|
|
Assert.True(fixture.Store.BindAttemptEndpoint(client).IsIdempotentReplay);
|
|
Assert.True(fixture.Store.ConsumeIntroduction(command.MediationHandle).Succeeded);
|
|
Assert.Equal(StoreResultCode.ReplayRejected, fixture.Store.ConsumeIntroduction(command.MediationHandle).Code);
|
|
Assert.Equal(StoreResultCode.ReplayRejected, fixture.Store.BindAttemptEndpoint(client with
|
|
{
|
|
PublicEndpoint = EphemeralStateFixture.OtherPublicEndpoint(40_021),
|
|
}).Code);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExpiredAttemptCannotBeObservedBoundOrConsumed()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
StoredListing listing = fixture.CreateVisibleListing(out _);
|
|
CreateJoinAttemptCommand command = fixture.AttemptCommand(listing);
|
|
fixture.Store.CreateJoinAttempt(command);
|
|
|
|
fixture.Clock.Advance(TimeSpan.FromSeconds(30));
|
|
|
|
Assert.Equal(StoreResultCode.NotFound, fixture.Store.BindAttemptEndpoint(new(
|
|
command.MediationHandle,
|
|
AttemptPeerRole.Client,
|
|
command.ClientCapabilityFingerprint,
|
|
EphemeralStateFixture.PublicEndpoint(40_050),
|
|
null)).Code);
|
|
Assert.Equal(StoreResultCode.NotFound, fixture.Store.ConsumeIntroduction(command.MediationHandle).Code);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenericReplayConsumptionIsBoundedAndExpires()
|
|
{
|
|
EphemeralStoreOptions options = new() { MaxReplayEntries = 1 };
|
|
EphemeralStateFixture fixture = new(options);
|
|
|
|
Assert.True(fixture.Store.ConsumeReplay(new("ticket", "one")).Succeeded);
|
|
Assert.Equal(StoreResultCode.ReplayRejected, fixture.Store.ConsumeReplay(new("ticket", "one")).Code);
|
|
Assert.Equal(StoreResultCode.CapacityExceeded, fixture.Store.ConsumeReplay(new("ticket", "two")).Code);
|
|
fixture.Clock.Advance(options.ReplayLifetime);
|
|
Assert.True(fixture.Store.ConsumeReplay(new("ticket", "two")).Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void PresenceAttemptAndRevocationPoolsShedWithoutPartialMutation()
|
|
{
|
|
EphemeralStoreOptions options = new()
|
|
{
|
|
MaxPresenceBindings = 1,
|
|
MaxJoinAttempts = 1,
|
|
MaxRevocations = 1,
|
|
};
|
|
EphemeralStateFixture fixture = new(options);
|
|
StoredListing first = fixture.CreateVisibleListing(out CreateListingCommand firstCommand);
|
|
CreateListingCommand secondCommand = fixture.ListingCommand(owner: "publisher-2");
|
|
fixture.Store.CreateListing(secondCommand);
|
|
|
|
Assert.Equal(StoreResultCode.CapacityExceeded, fixture.Store.BindHostPresence(new(
|
|
secondCommand.Listing.HostPresenceHandle,
|
|
secondCommand.Listing.HostPresenceFingerprint,
|
|
EphemeralStateFixture.OtherPublicEndpoint(42_000),
|
|
null)).Code);
|
|
Assert.True(fixture.Store.CreateJoinAttempt(fixture.AttemptCommand(first)).Succeeded);
|
|
Assert.Equal(StoreResultCode.CapacityExceeded, fixture.Store.CreateJoinAttempt(fixture.AttemptCommand(first, "client-2")).Code);
|
|
Assert.True(fixture.Store.RevokePrincipal("unrelated", TimeSpan.FromMinutes(1)).Succeeded);
|
|
Assert.Equal(StoreResultCode.CapacityExceeded, fixture.Store.RevokePrincipal(firstCommand.Listing.OwnerSubject, TimeSpan.FromMinutes(1)).Code);
|
|
Assert.True(fixture.Store.GetListing(first.Definition.ListingId, true).Succeeded);
|
|
Assert.True(fixture.Store.GetListing(secondCommand.Listing.ListingId, false).Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExhaustionShedsNewListingWithoutMutatingExistingState()
|
|
{
|
|
EphemeralStoreOptions options = new() { MaxListings = 1 };
|
|
EphemeralStateFixture fixture = new(options);
|
|
CreateListingCommand first = fixture.ListingCommand();
|
|
CreateListingCommand second = fixture.ListingCommand();
|
|
|
|
Assert.True(fixture.Store.CreateListing(first).Succeeded);
|
|
Assert.Equal(StoreResultCode.CapacityExceeded, fixture.Store.CreateListing(second).Code);
|
|
Assert.True(fixture.Store.GetListing(first.Listing.ListingId, false).Succeeded);
|
|
Assert.Equal(StoreResultCode.NotFound, fixture.Store.GetListing(second.Listing.ListingId, false).Code);
|
|
}
|
|
|
|
[Fact]
|
|
public void IdempotencyPoolExhaustionDoesNotCreateUntrackedResource()
|
|
{
|
|
EphemeralStoreOptions options = new() { MaxListings = 2, MaxIdempotencyEntries = 1 };
|
|
EphemeralStateFixture fixture = new(options);
|
|
CreateListingCommand first = fixture.ListingCommand();
|
|
CreateListingCommand second = fixture.ListingCommand();
|
|
|
|
Assert.True(fixture.Store.CreateListing(first).Succeeded);
|
|
Assert.Equal(StoreResultCode.CapacityExceeded, fixture.Store.CreateListing(second).Code);
|
|
Assert.True(fixture.Store.GetListing(first.Listing.ListingId, false).Succeeded);
|
|
Assert.Equal(StoreResultCode.NotFound, fixture.Store.GetListing(second.Listing.ListingId, false).Code);
|
|
}
|
|
|
|
[Fact]
|
|
public void PolicyQuotasAreCheckedInsideAtomicCreation()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
CreateListingCommand first = fixture.ListingCommand(owner: "publisher-quota") with { OwnerListingLimit = 1 };
|
|
CreateListingCommand second = fixture.ListingCommand(owner: "publisher-quota") with { OwnerListingLimit = 1 };
|
|
Assert.True(fixture.Store.CreateListing(first).Succeeded);
|
|
Assert.Equal(StoreResultCode.CapacityExceeded, fixture.Store.CreateListing(second).Code);
|
|
|
|
fixture.Store.BindHostPresence(new(
|
|
first.Listing.HostPresenceHandle,
|
|
first.Listing.HostPresenceFingerprint,
|
|
EphemeralStateFixture.PublicEndpoint(42_100),
|
|
null));
|
|
StoredListing listing = fixture.Store.GetListing(first.Listing.ListingId, true).Value!;
|
|
CreateJoinAttemptCommand attempt = fixture.AttemptCommand(listing) with { ScopeAttemptLimit = 1 };
|
|
Assert.True(fixture.Store.CreateJoinAttempt(attempt).Succeeded);
|
|
Assert.Equal(StoreResultCode.CapacityExceeded, fixture.Store.CreateJoinAttempt(
|
|
fixture.AttemptCommand(listing, "client-quota-2") with { ScopeAttemptLimit = 1 }).Code);
|
|
|
|
fixture.Clock.Advance(TimeSpan.FromSeconds(30));
|
|
Assert.True(fixture.Store.BindHostPresence(new(
|
|
first.Listing.HostPresenceHandle,
|
|
first.Listing.HostPresenceFingerprint,
|
|
EphemeralStateFixture.PublicEndpoint(42_101),
|
|
null)).Succeeded);
|
|
Assert.True(fixture.Store.CreateJoinAttempt(
|
|
fixture.AttemptCommand(listing, "client-quota-3") with { ScopeAttemptLimit = 1 }).Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemovingAListingReleasesItsConstantTimeOwnerQuota()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
CreateListingCommand first = fixture.ListingCommand(owner: "publisher-quota") with
|
|
{
|
|
OwnerListingLimit = 1,
|
|
};
|
|
CreateListingCommand second = fixture.ListingCommand(owner: "publisher-quota") with
|
|
{
|
|
OwnerListingLimit = 1,
|
|
};
|
|
StoreResult<StoredListing> created = fixture.Store.CreateListing(first);
|
|
Assert.True(created.Succeeded);
|
|
Assert.Equal(StoreResultCode.CapacityExceeded, fixture.Store.CreateListing(second).Code);
|
|
|
|
Assert.True(fixture.Store.DeleteListing(new(
|
|
first.Listing.ListingId,
|
|
first.Listing.LeaseId,
|
|
first.Listing.LeaseFingerprint,
|
|
first.Listing.OwnerSubject)).Succeeded);
|
|
Assert.True(fixture.Store.CreateListing(second).Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void RepeatedMutableDeadlineRefreshesKeepOneScheduledEntryPerKey()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
StoredListing listing = fixture.CreateVisibleListing(out _);
|
|
|
|
for (int iteration = 0; iteration < 10_000; iteration++)
|
|
{
|
|
fixture.Clock.Advance(TimeSpan.FromTicks(1));
|
|
StoreResult<StoredListing> renewed = fixture.Store.RenewLease(new(
|
|
listing.Definition.ListingId,
|
|
listing.Definition.LeaseId,
|
|
listing.Definition.LeaseFingerprint,
|
|
listing.Definition.OwnerSubject,
|
|
listing.Version));
|
|
Assert.True(renewed.Succeeded, $"Renewal {iteration} failed with {renewed.Code}.");
|
|
listing = renewed.Value!;
|
|
StoreResult<StoredListing> presence = fixture.Store.BindHostPresence(new(
|
|
listing.Definition.HostPresenceHandle,
|
|
listing.Definition.HostPresenceFingerprint,
|
|
EphemeralStateFixture.PublicEndpoint(42_200),
|
|
null));
|
|
Assert.True(presence.Succeeded, $"Presence {iteration} failed with {presence.Code}.");
|
|
StoreResult<int> revoked = fixture.Store.RevokePrincipal(
|
|
"repeated-revocation",
|
|
TimeSpan.FromMinutes(1));
|
|
Assert.True(revoked.Succeeded, $"Revocation {iteration} failed with {revoked.Code}.");
|
|
}
|
|
|
|
Assert.Equal(4, fixture.Store.ScheduledExpiryEntryCount);
|
|
fixture.Clock.Advance(TimeSpan.FromSeconds(19));
|
|
Assert.True(fixture.Store.GetListing(listing.Definition.ListingId, true).Succeeded);
|
|
Assert.Equal(4, fixture.Store.ScheduledExpiryEntryCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void RepeatedPrincipalRevocationCanExtendButCannotShortenProtection()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
const string subject = "protected-publisher";
|
|
|
|
Assert.True(fixture.Store.RevokePrincipal(subject, TimeSpan.FromMinutes(10)).Succeeded);
|
|
fixture.Clock.Advance(TimeSpan.FromSeconds(1));
|
|
Assert.True(fixture.Store.RevokePrincipal(subject, TimeSpan.FromSeconds(1)).Succeeded);
|
|
fixture.Clock.Advance(TimeSpan.FromSeconds(2));
|
|
Assert.Equal(
|
|
StoreResultCode.Revoked,
|
|
fixture.Store.CreateListing(fixture.ListingCommand(owner: subject)).Code);
|
|
|
|
fixture.Clock.Advance(TimeSpan.FromSeconds(597));
|
|
Assert.True(fixture.Store.CreateListing(fixture.ListingCommand(owner: subject)).Succeeded);
|
|
}
|
|
|
|
[Fact]
|
|
public void InvalidDefaultSecurityValuesCannotEnterStore()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
CreateListingCommand command = fixture.ListingCommand();
|
|
|
|
Assert.Throws<ArgumentException>(() => fixture.Store.CreateListing(command with
|
|
{
|
|
Listing = command.Listing with { LeaseFingerprint = default },
|
|
}));
|
|
}
|
|
|
|
[Fact]
|
|
public void RevocationRemovesEveryPathAndBlocksNewWorkAtomically()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
StoredListing listing = fixture.CreateVisibleListing(out CreateListingCommand command);
|
|
CreateJoinAttemptCommand attempt = fixture.AttemptCommand(listing, command.Listing.OwnerSubject);
|
|
fixture.Store.CreateJoinAttempt(attempt);
|
|
|
|
StoreResult<int> revoked = fixture.Store.RevokePrincipal(command.Listing.OwnerSubject, TimeSpan.FromMinutes(1));
|
|
|
|
Assert.True(revoked.Succeeded);
|
|
Assert.Equal(4, revoked.Value);
|
|
Assert.Equal(StoreResultCode.NotFound, fixture.Store.GetListing(listing.Definition.ListingId, false).Code);
|
|
Assert.Equal(StoreResultCode.NotFound, fixture.Store.BindAttemptEndpoint(new(
|
|
attempt.MediationHandle,
|
|
AttemptPeerRole.Client,
|
|
attempt.ClientCapabilityFingerprint,
|
|
EphemeralStateFixture.PublicEndpoint(40_030),
|
|
null)).Code);
|
|
Assert.Equal(StoreResultCode.Revoked, fixture.Store.CreateListing(fixture.ListingCommand(owner: command.Listing.OwnerSubject)).Code);
|
|
}
|
|
|
|
[Fact]
|
|
public void RestartHasNewGenerationAndNoEphemeralState()
|
|
{
|
|
EphemeralStateFixture before = new();
|
|
StoredListing listing = before.CreateVisibleListing(out _);
|
|
EphemeralStateFixture after = new();
|
|
|
|
Assert.NotEqual(before.Store.InstanceId, after.Store.InstanceId);
|
|
Assert.Equal(StoreResultCode.NotFound, after.Store.GetListing(listing.Definition.ListingId, false).Code);
|
|
}
|
|
|
|
[Fact]
|
|
public void DrainRejectsNewWorkAllowsInflightCompletionThenClearsState()
|
|
{
|
|
EphemeralStoreOptions options = new() { GracefulDrainLifetime = TimeSpan.FromSeconds(5) };
|
|
EphemeralStateFixture fixture = new(options);
|
|
StoredListing listing = fixture.CreateVisibleListing(out _);
|
|
CreateJoinAttemptCommand attempt = fixture.AttemptCommand(listing);
|
|
fixture.Store.CreateJoinAttempt(attempt);
|
|
fixture.Store.BeginDrain();
|
|
|
|
Assert.Equal(StoreResultCode.Draining, fixture.Store.CreateListing(fixture.ListingCommand()).Code);
|
|
Assert.Equal(StoreResultCode.Draining, fixture.Store.CreateJoinAttempt(fixture.AttemptCommand(listing)).Code);
|
|
Assert.True(fixture.Store.BindAttemptEndpoint(new(
|
|
attempt.MediationHandle,
|
|
AttemptPeerRole.Client,
|
|
attempt.ClientCapabilityFingerprint,
|
|
EphemeralStateFixture.PublicEndpoint(41_000),
|
|
null)).Succeeded);
|
|
|
|
fixture.Clock.Advance(options.GracefulDrainLifetime);
|
|
Assert.Equal(StoreResultCode.NotFound, fixture.Store.GetListing(listing.Definition.ListingId, false).Code);
|
|
Assert.Equal(StoreResultCode.NotFound, fixture.Store.BindAttemptEndpoint(new(
|
|
attempt.MediationHandle,
|
|
AttemptPeerRole.Host,
|
|
attempt.HostCapabilityFingerprint,
|
|
EphemeralStateFixture.PublicEndpoint(41_001),
|
|
null)).Code);
|
|
}
|
|
|
|
[Fact]
|
|
public void PrecancelledOperationHasNoPartialEffect()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
CreateListingCommand command = fixture.ListingCommand();
|
|
using CancellationTokenSource cancellation = new();
|
|
cancellation.Cancel();
|
|
|
|
Assert.Throws<OperationCanceledException>(() => fixture.Store.CreateListing(command, cancellation.Token));
|
|
Assert.Equal(StoreResultCode.NotFound, fixture.Store.GetListing(command.Listing.ListingId, false).Code);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnavailableStoreFailsNewAuthorizationClosedAndErasesActiveState()
|
|
{
|
|
EphemeralStateFixture fixture = new();
|
|
StoredListing listing = fixture.CreateVisibleListing(out _);
|
|
fixture.Store.MarkUnavailable();
|
|
|
|
Assert.Equal(StoreResultCode.ServiceUnavailable, fixture.Store.GetListing(listing.Definition.ListingId, false).Code);
|
|
Assert.Equal(StoreResultCode.ServiceUnavailable, fixture.Store.CreateJoinAttempt(fixture.AttemptCommand(listing)).Code);
|
|
}
|
|
}
|