Refactor user ID access in SignalR hubs and add online user tracking

Introduces IHubUserAccessor and its implementation to centralize user ID retrieval from SignalR HubCallerContext, replacing duplicated logic in ChatsHub, FriendsHub, and PresenceHub. Moves extension and mapping files to a Common directory, adds UserToUserDtoMappingAction for online status mapping, and implements OnlineUserStore with tests for tracking online users. Updates dependency injection and test code to use the new abstractions.
This commit is contained in:
Artemy
2025-07-23 22:21:51 +07:00
parent 31fdf4cb37
commit be0edb0f94
17 changed files with 326 additions and 96 deletions
@@ -0,0 +1,114 @@
using System.Security.Claims;
using Govor.API.Common.SignalR.Helpers;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Moq;
namespace Govor.API.Tests.Common.SignalR.Helpers;
[TestFixture]
[TestOf(typeof(HubUserAccessor))]
public class HubUserAccessorTests
{
private HubUserAccessor _accessor;
private Mock<ILogger<HubUserAccessor>> _loggerMock;
[SetUp]
public void SetUp()
{
_loggerMock = new Mock<ILogger<HubUserAccessor>>();
_accessor = new HubUserAccessor(_loggerMock.Object);
}
private HubCallerContext CreateContextWithClaims(params Claim[] claims)
{
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims));
var contextMock = new Mock<HubCallerContext>();
contextMock.Setup(c => c.User).Returns(principal);
return contextMock.Object;
}
[Test]
public void GetUserId_ValidClaim_ReturnsGuid()
{
// Arrange
var expectedGuid = Guid.NewGuid();
var context = CreateContextWithClaims(new Claim("userId", expectedGuid.ToString()));
// Act
var result = _accessor.GetUserId(context);
// Assert
Assert.That(expectedGuid, Is.EqualTo(result));
}
[Test]
public void GetUserId_InvalidClaim_ThrowsException_WhenNotSuppressed()
{
// Arrange
var context = CreateContextWithClaims(new Claim("userId", "not-a-guid"));
// Act & Assert
Assert.Throws<UnauthorizedAccessException>(() => _accessor.GetUserId(context, suppressException: false));
}
[Test]
public void GetUserId_InvalidClaim_ReturnsEmptyGuid_WhenSuppressed()
{
// Arrange
var context = CreateContextWithClaims(new Claim("userId", "not-a-guid"));
// Act
var result = _accessor.GetUserId(context, suppressException: true);
// Assert
Assert.That(Guid.Empty, Is.EqualTo(result));
}
[Test]
public void GetUserId_NoClaim_ThrowsException_WhenNotSuppressed()
{
// Arrange
var context = CreateContextWithClaims();
// Act & Assert
Assert.Throws<UnauthorizedAccessException>(() => _accessor.GetUserId(context, suppressException: false));
}
[Test]
public void GetUserId_NoClaim_ReturnsEmptyGuid_WhenSuppressed()
{
// Arrange
var context = CreateContextWithClaims();
// Act
var result = _accessor.GetUserId(context, suppressException: true);
// Assert
Assert.That(Guid.Empty, Is.EqualTo(result));
}
[Test]
public void GetUserId_UserIsNull_ReturnsEmptyGuid_WhenSuppressed()
{
// Arrange
var contextMock = new Mock<HubCallerContext>();
contextMock.Setup(c => c.User).Returns((ClaimsPrincipal?)null);
// Act
var result = _accessor.GetUserId(contextMock.Object, suppressException: true);
// Assert
Assert.That(Guid.Empty, Is.EqualTo(result));
}
[Test]
public void GetUserId_UserIsNull_ThrowsException_WhenNotSuppressed()
{
// Arrange
var contextMock = new Mock<HubCallerContext>();
contextMock.Setup(c => c.User).Returns((ClaimsPrincipal?)null);
// Act & Assert
Assert.Throws<UnauthorizedAccessException>(() => _accessor.GetUserId(contextMock.Object, suppressException: false));
}
}
@@ -1,4 +1,5 @@
using AutoFixture;
using Govor.API.Common.SignalR.Helpers;
using Govor.API.Hubs;
using Govor.Application.Interfaces;
using Govor.Application.Interfaces.Messages;
@@ -13,6 +14,7 @@ public class ChatsHubTests
private Mock<ILogger<ChatsHub>> _loggerMock;
private Mock<IMessageCommandService> _messageServiceMock;
private Mock<IUserGroupsService> _userGroupsServiceMock;
private Mock<IHubUserAccessor> _hubUserAccessorMock;
private Fixture _fixture;
private ChatsHub _chatsHub;
@@ -26,11 +28,13 @@ public class ChatsHubTests
_messageServiceMock = new Mock<IMessageCommandService>();
_userGroupsServiceMock = new Mock<IUserGroupsService>();
_loggerMock = new Mock<ILogger<ChatsHub>>();
_hubUserAccessorMock = new Mock<IHubUserAccessor>();
_chatsHub = new ChatsHub(
_loggerMock.Object,
_messageServiceMock.Object,
_userGroupsServiceMock.Object
_userGroupsServiceMock.Object,
_hubUserAccessorMock.Object
);
}
@@ -1,5 +1,6 @@
using AutoFixture;
using AutoMapper;
using Govor.API.Common.SignalR.Helpers;
using Govor.API.Hubs;
using Govor.Application.Exceptions.FriendsService;
using Govor.Application.Interfaces.Friends;
@@ -17,7 +18,7 @@ namespace Govor.API.Tests.IntegrationTests.Hubs;
public class FriendsHubTests
{
private Mock<IFriendRequestCommandService> _friendRequestServiceMock = null!;
private Mock<ICurrentUserService> _currentUserServiceMock = null!;
private Mock<IHubUserAccessor> _currentUserServiceMock = null!;
private Mock<IHubCallerClients> _clientsMock = null!;
private Mock<IClientProxy> _clientProxyMock = null!;
private Mock<ILogger<FriendsHub>> _loggerMock = null!;
@@ -35,19 +36,23 @@ public class FriendsHubTests
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_friendRequestServiceMock = new Mock<IFriendRequestCommandService>();
_currentUserServiceMock = new Mock<ICurrentUserService>();
_currentUserServiceMock = new Mock<IHubUserAccessor>();
_clientsMock = new Mock<IHubCallerClients>();
_clientProxyMock = new Mock<IClientProxy>();
_loggerMock = new Mock<ILogger<FriendsHub>>();
_mapperMock = new Mock<IMapper>();
_currentUserServiceMock.Setup(x => x.GetCurrentUserId()).Returns(_userId);
_currentUserServiceMock.Setup(x => x.GetUserId(
It.IsAny<HubCallerContext>(),
It.IsAny<bool>()))
.Returns(_userId);
_clientsMock.Setup(c => c.Group(It.IsAny<string>())).Returns(_clientProxyMock.Object);
_hub = new FriendsHub(
_loggerMock.Object,
_friendRequestServiceMock.Object,
_currentUserServiceMock.Object,
_loggerMock.Object,
_mapperMock.Object)
{
Clients = _clientsMock.Object
@@ -129,7 +134,7 @@ public class FriendsHubTests
public async Task SendRequest_ShouldReturnUnauthorized_WhenCurrentUserIsNotAuthenticated()
{
// Arrange
_currentUserServiceMock.Setup(x => x.GetCurrentUserId())
_currentUserServiceMock.Setup(x => x.GetUserId(It.IsAny<HubCallerContext>(), It.IsAny<bool>()))
.Throws(new UnauthorizedAccessException("userId claim is missing or invalid"));
// Act