diff --git a/SampleMultiplayerClient/SampleMultiplayerClient.csproj b/SampleMultiplayerClient/SampleMultiplayerClient.csproj
index d33aa350..488db391 100644
--- a/SampleMultiplayerClient/SampleMultiplayerClient.csproj
+++ b/SampleMultiplayerClient/SampleMultiplayerClient.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/SampleSpectatorClient/SampleSpectatorClient.csproj b/SampleSpectatorClient/SampleSpectatorClient.csproj
index d33aa350..488db391 100644
--- a/SampleSpectatorClient/SampleSpectatorClient.csproj
+++ b/SampleSpectatorClient/SampleSpectatorClient.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/osu.Server.Spectator.Tests/ConcurrentConnectionLimiterTests.cs b/osu.Server.Spectator.Tests/ConcurrentConnectionLimiterTests.cs
index c5c37e4d..7d5ce52a 100644
--- a/osu.Server.Spectator.Tests/ConcurrentConnectionLimiterTests.cs
+++ b/osu.Server.Spectator.Tests/ConcurrentConnectionLimiterTests.cs
@@ -5,9 +5,12 @@
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Connections.Features;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Moq;
+using osu.Game.Online;
using osu.Server.Spectator.Entities;
using osu.Server.Spectator.Hubs.Spectator;
using Xunit;
@@ -37,8 +40,301 @@ public ConcurrentConnectionLimiterTests()
hubMock = new Mock();
}
+ #region New path (uses client-side generated session GUID)
+
+ [Fact]
+ public async Task TestNormalOperation_SessionIDPresent()
+ {
+ var hubCallerContextMock = new Mock();
+ var httpContextMock = new Mock();
+ hubCallerContextMock.Setup(ctx => ctx.UserIdentifier).Returns("1234");
+ hubCallerContextMock.Setup(ctx => ctx.User).Returns(new ClaimsPrincipal(new[]
+ {
+ new ClaimsIdentity(new[]
+ {
+ new Claim("jti", Guid.NewGuid().ToString())
+ })
+ }));
+ hubCallerContextMock.Setup(ctx => ctx.Features.Get()).Returns(httpContextMock.Object);
+ httpContextMock.Setup(ctx => ctx.HttpContext).Returns(() =>
+ {
+ var context = new DefaultHttpContext();
+ context.Request.Headers[HubClientConnector.CLIENT_SESSION_ID_HEADER] = Guid.NewGuid().ToString();
+ return context;
+ });
+
+ var filter = new ConcurrentConnectionLimiter(connectionStates, serviceProviderMock.Object, loggerFactoryMock.Object);
+ var lifetimeContext = new HubLifetimeContext(hubCallerContextMock.Object, serviceProviderMock.Object, hubMock.Object);
+
+ bool connected = false;
+ await filter.OnConnectedAsync(lifetimeContext, _ =>
+ {
+ connected = true;
+ return Task.CompletedTask;
+ });
+ Assert.True(connected);
+ Assert.Single(connectionStates.GetEntityUnsafe(1234)!.ConnectionIds);
+
+ bool methodInvoked = false;
+ var invocationContext = new HubInvocationContext(hubCallerContextMock.Object, serviceProviderMock.Object, hubMock.Object,
+ typeof(SpectatorHub).GetMethod(nameof(SpectatorHub.StartWatchingUser))!, new object[] { 1234 });
+ await filter.InvokeMethodAsync(invocationContext, _ =>
+ {
+ methodInvoked = true;
+ return new ValueTask