Files
Rendezvous/tests/FinalFactory.Rendezvous.Tests/ConnectionOutcomes/ConnectionOutcomeServiceTests.cs
T
KyuubiYoru 94aba8a3bb
quality-gate / quality (push) Successful in 59s
feat(client): standardize connection outcomes (#13)
2026-07-16 10:18:41 +02:00

176 lines
6.7 KiB
C#

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