Files
Radicade/ServerTests/ConnectionRecoveryTests.cs
T
2026-10-16 23:33:13 -07:00

111 lines
4.7 KiB
C#

using System.Security.Claims;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using RadicadeServer.API.Notifications;
using RadicadeServer.DB;
using RadicadeServer.RecNet.Accounts;
using Xunit;
namespace RadicadeServer.Tests;
[Collection(DbFactoryCollection.Name)]
public class ConnectionRecoveryTests
{
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task ReconnectReplaysActiveBanWithoutRoomPresence(bool active)
{
var options = TestDbOptions.Create()
.UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
await using var db = new RecDBContext(options);
db.Moderation.Add(new RadicadeServer.RecNet.Moderation.ModerationDB
{
PlayerId = 987654322, IsBan = true, Message = "test ban",
ExpireDuration = DateTime.UtcNow.AddHours(active ? 1 : -1)
});
await db.SaveChangesAsync();
var proxy = new RecordingClient();
var clients = System.Reflection.DispatchProxy.Create<IHubCallerClients, CallerClientsProxy>();
((CallerClientsProxy)(object)clients).Client = proxy;
var previousContext = Notify.Context;
var previousFactory = Notify.DbFactory;
try
{
Notify.DbFactory = null!;
var hub = new NotifyHub(previousContext, db)
{
Context = new TestHubContext(987654322), Clients = clients
};
await hub.OnConnectedAsync();
Assert.Equal(active ? 1 : 0, proxy.Messages.Count);
if (active)
{
Assert.Equal("Notification", proxy.Messages[0].Method);
// The replayed kick must carry the client enum's NUMBER (22) inside a JSON
// string; the name parses and lands on no handler, and a bare number does not
// parse at all - both leave a banned player kicked with no ban screen.
Assert.Contains("\"Id\":\"22\"", proxy.Messages[0].Json);
Assert.DoesNotContain("ModerationKick", proxy.Messages[0].Json);
Assert.Contains("test ban", proxy.Messages[0].Json);
}
}
finally
{
lock (Notify.ClientsLock) Notify.Clients.RemoveAll(c => c.AccountId == 987654322);
Notify.Context = previousContext;
Notify.DbFactory = previousFactory;
}
}
public class CallerClientsProxy : System.Reflection.DispatchProxy
{
public IClientProxy Client = null!;
protected override object? Invoke(System.Reflection.MethodInfo? method, object?[]? args)
=> method?.Name == "get_Caller" ? Client : throw new NotSupportedException();
}
private sealed class RecordingClient : ISingleClientProxy
{
public Task<T> InvokeCoreAsync<T>(string method, object?[] args, CancellationToken cancellationToken = default)
=> throw new NotSupportedException();
public List<(string Method, string Json)> Messages { get; } = new();
public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default)
{
Messages.Add((method, Assert.IsType<string>(args[0])));
return Task.CompletedTask;
}
}
[Fact]
public async Task NotificationDisconnectDoesNotChangeGamePresence()
{
var options = TestDbOptions.Create()
.UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
await using var db = new RecDBContext(options);
var presence = new RealPresence { playerId = 987654321, isOnline = true, roomInstance = 42 };
db.Presences.Add(presence);
await db.SaveChangesAsync();
var hub = new NotifyHub(Notify.Context, db) { Context = new TestHubContext(presence.playerId) };
await hub.OnDisconnectedAsync(new IOException("Transport interrupted"));
await db.Entry(presence).ReloadAsync();
Assert.Equal(42L, presence.roomInstance);
Assert.True(presence.isOnline);
}
private sealed class TestHubContext(long accountId) : HubCallerContext
{
public override string ConnectionId => "connection-recovery-test";
public override string? UserIdentifier => accountId.ToString();
public override ClaimsPrincipal User => new(new ClaimsIdentity(
[new Claim(ClaimTypes.NameIdentifier, accountId.ToString())], "test"));
public override IDictionary<object, object?> Items { get; } = new Dictionary<object, object?>();
public override IFeatureCollection Features { get; } = new FeatureCollection();
public override CancellationToken ConnectionAborted => CancellationToken.None;
public override void Abort() { }
}
}