using FinalFactory.Rendezvous.Contracts; using FinalFactory.Rendezvous.Server.ConnectionOutcomes; using FinalFactory.Rendezvous.Tests.JoinAttempts; namespace FinalFactory.Rendezvous.Tests.ConnectionOutcomes; public sealed class ConnectionOutcomeServiceTests { [Fact] public void ReportRemainsAuthenticatedAfterAttemptExpiryAndCountsOnlyOnce() { using JoinAttemptFixture fixture = new(); (RegisterSessionResponse registration, _) = fixture.CreateHost(); CreateJoinAttemptResponse attempt = fixture.Create(registration.ListingId); ConnectionOutcomeMetrics metrics = new(); ConnectionOutcomeService service = new( fixture.Sessions.Store, fixture.Sessions.Capabilities, metrics); ReportConnectionOutcomeRequest report = new() { Outcome = ConnectionOutcomeKind.PunchTimedOut, ElapsedBucket = ConnectionElapsedBucket.FiveToFifteenSeconds, }; fixture.Sessions.Clock.Advance(fixture.Sessions.StoreOptions.JoinAttemptLifetime); ConnectionOutcomeServiceResult first = service.Report( attempt.AttemptId, attempt.ClientPunchCapability, report); ConnectionOutcomeServiceResult duplicate = service.Report( attempt.AttemptId, attempt.ClientPunchCapability, report); ConnectionOutcomeServiceResult conflict = service.Report( attempt.AttemptId, attempt.ClientPunchCapability, new ReportConnectionOutcomeRequest { Outcome = ConnectionOutcomeKind.Connected, ElapsedBucket = ConnectionElapsedBucket.FiveToFifteenSeconds, }); Assert.True(first.Succeeded); Assert.False(first.Value!.IsDuplicate); Assert.True(duplicate.Succeeded); Assert.True(duplicate.Value!.IsDuplicate); Assert.Equal(RendezvousErrorCode.ReplayRejected, conflict.Error); Assert.Equal( 1, metrics.GetCount( ConnectionOutcomeKind.PunchTimedOut, ConnectionElapsedBucket.FiveToFifteenSeconds)); Assert.Equal( RendezvousErrorCode.NotFound, service.Report( attempt.AttemptId, new string('X', ContractLimits.DerivedCredentialCharacters), report).Error); } [Theory] [InlineData(ConnectionOutcomeKind.DirectoryNotFound)] [InlineData(ConnectionOutcomeKind.IncompatibleProtocol)] [InlineData(ConnectionOutcomeKind.Unauthorized)] [InlineData(ConnectionOutcomeKind.RateLimited)] public void ReportRejectsOutcomesThatCouldNotHaveAnIssuedAttempt( ConnectionOutcomeKind outcome) { using JoinAttemptFixture fixture = new(); (RegisterSessionResponse registration, _) = fixture.CreateHost(); CreateJoinAttemptResponse attempt = fixture.Create(registration.ListingId); ConnectionOutcomeService service = new( fixture.Sessions.Store, fixture.Sessions.Capabilities, new ConnectionOutcomeMetrics()); ConnectionOutcomeServiceResult result = service.Report( attempt.AttemptId, attempt.ClientPunchCapability, new ReportConnectionOutcomeRequest { Outcome = outcome, ElapsedBucket = ConnectionElapsedBucket.UnderOneSecond, }); Assert.Equal(RendezvousErrorCode.InvalidRequest, result.Error); } [Fact] public void ListingDeletionRemovesRetainedOutcomeAuthorization() { using JoinAttemptFixture fixture = new(); (RegisterSessionResponse registration, _) = fixture.CreateHost(); CreateJoinAttemptResponse attempt = fixture.Create(registration.ListingId); ConnectionOutcomeService service = new( fixture.Sessions.Store, fixture.Sessions.Capabilities, new ConnectionOutcomeMetrics()); Assert.True(fixture.Sessions.Store.RevokeListing(registration.ListingId).Succeeded); Assert.Equal( RendezvousErrorCode.NotFound, service.Report( attempt.AttemptId, attempt.ClientPunchCapability, new ReportConnectionOutcomeRequest { Outcome = ConnectionOutcomeKind.Cancelled, ElapsedBucket = ConnectionElapsedBucket.UnderOneSecond, }).Error); } [Fact] public void PrincipalRevocationRemovesReportAuthorizationAfterAttemptExpiry() { using JoinAttemptFixture fixture = new(); (RegisterSessionResponse registration, _) = fixture.CreateHost(); CreateJoinAttemptResponse attempt = fixture.Create(registration.ListingId); ConnectionOutcomeService service = new( fixture.Sessions.Store, fixture.Sessions.Capabilities, new ConnectionOutcomeMetrics()); fixture.Sessions.Clock.Advance(fixture.Sessions.StoreOptions.JoinAttemptLifetime); Assert.True(fixture.Sessions.Store.RevokePrincipal( fixture.ClientSubject, TimeSpan.FromMinutes(1)).Succeeded); Assert.Equal( RendezvousErrorCode.NotFound, service.Report( attempt.AttemptId, attempt.ClientPunchCapability, new ReportConnectionOutcomeRequest { Outcome = ConnectionOutcomeKind.Cancelled, ElapsedBucket = ConnectionElapsedBucket.UnderOneSecond, }).Error); } [Fact] public void FrozenV1ReportFieldsAreAcceptedButNormalizedBeforeRetention() { using JoinAttemptFixture fixture = new(); (RegisterSessionResponse registration, _) = fixture.CreateHost(); CreateJoinAttemptResponse attempt = fixture.Create(registration.ListingId); ConnectionOutcomeMetrics metrics = new(); ConnectionOutcomeService service = new( fixture.Sessions.Store, fixture.Sessions.Capabilities, metrics); #pragma warning disable CS0618 // Deliberately exercises the frozen legacy input surface. ReportConnectionOutcomeRequest legacy = new() { Outcome = ConnectionOutcomeKind.TimedOut, ElapsedMilliseconds = 6_000, DiagnosticCode = "legacy-text-is-discarded", }; #pragma warning restore CS0618 ConnectionOutcomeServiceResult result = service.Report( attempt.AttemptId, attempt.ClientPunchCapability, legacy); Assert.True(result.Succeeded); Assert.Equal( 1, metrics.GetCount( ConnectionOutcomeKind.PunchTimedOut, ConnectionElapsedBucket.FiveToFifteenSeconds)); } }