Minecraft-Console-Client/MinecraftClient.Tests/McClientConnectionFailureTests.cs
Anon 5655e3fc89 Fix restart publication races (#3186)
Prepare offline routing before restart requests become observable, reject stale route owners, and wait for source-client cleanup before executing automatic retries.

Add deterministic publication-order, cleanup-gate, and stale-route regression tests.
2026-07-27 17:26:52 +02:00

122 lines
3.8 KiB
C#

using MinecraftClient.Scripting;
namespace MinecraftClient.Tests;
public sealed class McClientConnectionFailureTests
{
[Fact]
public void LoginRejectedClaimPreventsSyntheticConnectionLostFallback()
{
var lifecycle = new ConnectionAttemptLifecycle();
Assert.True(lifecycle.TryBeginDisconnect());
Assert.True(lifecycle.IsFailureClaimed);
Assert.False(lifecycle.TryBeginDisconnect());
lifecycle.CompleteDisconnect();
Assert.True(lifecycle.IsFailureClaimed);
Assert.False(lifecycle.TryBeginDisconnect());
}
[Fact]
public void UnclaimedGenericFailureCanBeClaimedExactlyOnce()
{
var lifecycle = new ConnectionAttemptLifecycle();
Assert.False(lifecycle.IsFailureClaimed);
Assert.True(lifecycle.TryBeginDisconnect());
Assert.False(lifecycle.TryBeginDisconnect());
}
[Fact]
public void HeldBotsAreRestoredBeforeFailureAndReceiveOriginalMessageOnce()
{
const string rejectionMessage = "You are not white-listed on this server!";
var bot = new RecordingBot();
List<ChatBot> heldBots = [bot];
var loadedBots = new List<ChatBot>();
ConnectionAttemptLifecycle.RestoreHeldBots(heldBots, loadedBots.Add);
foreach (ChatBot loadedBot in loadedBots)
loadedBot.OnDisconnect(ChatBot.DisconnectReason.LoginRejected, rejectionMessage);
Assert.Empty(heldBots);
Assert.Single(loadedBots);
Assert.Equal(1, bot.DisconnectCount);
Assert.Equal(ChatBot.DisconnectReason.LoginRejected, bot.LastReason);
Assert.Equal(rejectionMessage, bot.LastMessage);
}
[Fact]
public void OfflineRouteStaysOwnedAcrossReplacementAndSuccessfulHandoff()
{
var route = new AttemptOwnedRoute();
int activations = 0;
int deactivations = 0;
Assert.True(route.TryActivate(7, 7, () => activations++));
Assert.False(route.TryActivate(7, 7, () => activations++));
Assert.True(route.TryTransfer(7, 8));
Assert.False(route.TryDeactivate(7, () => deactivations++));
Assert.Equal(8, route.OwnerAttempt);
Assert.True(route.TryDeactivate(8, () => deactivations++));
Assert.Equal(1, activations);
Assert.Equal(1, deactivations);
Assert.Equal(-1, route.OwnerAttempt);
}
[Fact]
public void InitialConnectionAttemptCanOwnOfflineRoute()
{
var route = new AttemptOwnedRoute();
int activations = 0;
Assert.True(route.TryActivate(0, 0, () => activations++));
Assert.Equal(1, activations);
Assert.Equal(0, route.OwnerAttempt);
}
[Fact]
public void OlderAttemptCannotTakeAnEmptyOfflineRoute()
{
var route = new AttemptOwnedRoute();
int activations = 0;
Assert.False(route.TryActivate(4, 5, () => activations++));
Assert.Equal(0, activations);
Assert.Equal(-1, route.OwnerAttempt);
}
[Fact]
public void StaleCleanupCannotClearNewerOfflineRoute()
{
var route = new AttemptOwnedRoute();
int deactivations = 0;
Assert.True(route.TryActivate(10, 10, () => { }));
Assert.True(route.TryActivate(11, 11, () => { }));
Assert.False(route.TryDeactivate(10, () => deactivations++));
Assert.Equal(11, route.OwnerAttempt);
Assert.Equal(0, deactivations);
}
private sealed class RecordingBot : ChatBot
{
internal int DisconnectCount { get; private set; }
internal DisconnectReason? LastReason { get; private set; }
internal string? LastMessage { get; private set; }
public override bool OnDisconnect(DisconnectReason reason, string message)
{
DisconnectCount++;
LastReason = reason;
LastMessage = message;
return false;
}
}
}