This commit is contained in:
Artemy
2025-07-24 13:43:51 +07:00
parent d5299f2b79
commit caefcd5827
3 changed files with 116 additions and 14 deletions
+80
View File
@@ -2,6 +2,8 @@ using AutoFixture;
using Govor.API.Common.SignalR.Helpers; using Govor.API.Common.SignalR.Helpers;
using Govor.API.Hubs; using Govor.API.Hubs;
using Govor.Application.Interfaces.UserOnlineStatus; using Govor.Application.Interfaces.UserOnlineStatus;
using Govor.Core.Repositories.Users;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moq; using Moq;
@@ -16,11 +18,89 @@ public class PresenceHubTests
private Mock<IUserNotificationScopeService> _mockUserNotification; private Mock<IUserNotificationScopeService> _mockUserNotification;
private Mock<IOnlineUserStore> _mockOnlineUserStore; private Mock<IOnlineUserStore> _mockOnlineUserStore;
private Mock<IHubUserAccessor> _mockHubUserAccessor; private Mock<IHubUserAccessor> _mockHubUserAccessor;
private Mock<IUsersRepository> _mockUserRepository;
private Mock<IHubCallerClients> _clientsMock = null!;
private Mock<IClientProxy> _clientProxyMock = null!;
private Mock<HubCallerContext> _mockContext = null!;
private Mock<IGroupManager> _groupManagerMock = null!;
private Fixture _fixture; private Fixture _fixture;
private PresenceHub _hub;
private Guid _userId;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
_fixture = new Fixture();
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList().ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_mockLogger = new Mock<ILogger<PresenceHub>>();
_mockUserNotification = new Mock<IUserNotificationScopeService>();
_mockOnlineUserStore = new Mock<IOnlineUserStore>();
_mockHubUserAccessor = new Mock<IHubUserAccessor>();
_mockUserRepository = new Mock<IUsersRepository>();
_clientsMock = new Mock<IHubCallerClients>();
_clientProxyMock = new Mock<IClientProxy>();
_userId = Guid.NewGuid();
_mockHubUserAccessor.Setup(h => h.GetUserId(
It.IsAny<HubCallerContext>(),
It.IsAny<bool>()))
.Returns(_userId);
_clientsMock.Setup(c => c.Group(It.IsAny<string>())).Returns(_clientProxyMock.Object);
_mockContext = new Mock<HubCallerContext>();
_mockContext.Setup(c => c.ConnectionId).Returns("test-connection");
_groupManagerMock = new Mock<IGroupManager>();
_groupManagerMock.Setup(g => g.AddToGroupAsync(It.IsAny<string>(), It.IsAny<string>(), default))
.Returns(Task.CompletedTask);
_hub = new PresenceHub(
_mockLogger.Object,
_mockUserNotification.Object,
_mockOnlineUserStore.Object,
_mockHubUserAccessor.Object,
_mockUserRepository.Object)
{
Clients = _clientsMock.Object,
Context = _mockContext.Object,
Groups = _groupManagerMock.Object,
};
} }
// Tests for OnConnectedAsync action
[Test]
public async Task OnConnectedAsync_SetsUserOnlineAndNotifiesFriends()
{
// Arrange
_mockUserRepository.Setup(f => f.ExistsByIdAsync(_userId))
.ReturnsAsync(true);
var friendsIds = _fixture.CreateMany<Guid>().ToList();
_mockUserNotification.Setup(n => n.GetNotifiedUsers(_userId))
.ReturnsAsync(friendsIds);
// Act
await _hub.OnConnectedAsync();
// Assert
_mockOnlineUserStore.Verify(store => store.SetOnlineUser(_userId), Times.Once);
_clientsMock.Verify(c => c.Group(It.IsAny<string>()), Times.Exactly(friendsIds.Count));
foreach (var friendId in friendsIds)
{
_clientProxyMock.Verify(proxy =>
proxy.SendCoreAsync("UserOnline",
It.Is<object[]>(args => args.Length == 1 && (Guid)args[0] == _userId),
It.IsAny<CancellationToken>()),
Times.Exactly(friendsIds.Count));
}
}
} }
-1
View File
@@ -49,7 +49,6 @@ public class FriendsHub : Hub
var userId = _userAccessor.GetUserId(Context, true); var userId = _userAccessor.GetUserId(Context, true);
if (userId != Guid.Empty) if (userId != Guid.Empty)
{ {
// Remove user from their own group
await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString()); await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString());
_logger.LogInformation( _logger.LogInformation(
"User {UserId} disconnected with ConnectionId {ConnectionId} and removed from their group", userId, "User {UserId} disconnected with ConnectionId {ConnectionId} and removed from their group", userId,
+36 -13
View File
@@ -1,5 +1,6 @@
using Govor.API.Common.SignalR.Helpers; using Govor.API.Common.SignalR.Helpers;
using Govor.Application.Interfaces.UserOnlineStatus; using Govor.Application.Interfaces.UserOnlineStatus;
using Govor.Core.Repositories.Users;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
@@ -11,18 +12,21 @@ namespace Govor.API.Hubs;
public class PresenceHub : Hub public class PresenceHub : Hub
{ {
private readonly ILogger<PresenceHub> _logger; private readonly ILogger<PresenceHub> _logger;
private readonly IUserNotificationScopeService _notificationScopeService; private readonly IUserNotificationScopeService _scopeService;
private readonly IOnlineUserStore _onlineUserStore; private readonly IOnlineUserStore _onlineUserStore;
private readonly IHubUserAccessor _userAccessor; private readonly IHubUserAccessor _userAccessor;
private readonly IUsersRepository _users;
public PresenceHub( public PresenceHub(
ILogger<PresenceHub> logger, ILogger<PresenceHub> logger,
IUserNotificationScopeService notificationScopeService, IUserNotificationScopeService scopeService,
IOnlineUserStore onlineUserStore, IOnlineUserStore onlineUserStore,
IHubUserAccessor userAccessor) IHubUserAccessor userAccessor,
IUsersRepository users)
{ {
_logger = logger; _logger = logger;
_notificationScopeService = notificationScopeService; _users = users;
_scopeService = scopeService;
_onlineUserStore = onlineUserStore; _onlineUserStore = onlineUserStore;
_userAccessor = userAccessor; _userAccessor = userAccessor;
} }
@@ -30,9 +34,9 @@ public class PresenceHub : Hub
public override async Task OnConnectedAsync() public override async Task OnConnectedAsync()
{ {
var userId = _userAccessor.GetUserId(Context); var userId = _userAccessor.GetUserId(Context);
if (userId == Guid.Empty) if (userId == Guid.Empty || await _users.ExistsByIdAsync(userId) == false)
{ {
_logger.LogWarning("User connected with invalid UserID claim."); _logger.LogWarning("User connected with invalid UserId claim.");
Context.Abort(); Context.Abort();
return; return;
} }
@@ -40,7 +44,7 @@ public class PresenceHub : Hub
_onlineUserStore.SetOnlineUser(userId); _onlineUserStore.SetOnlineUser(userId);
await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString()); await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString());
var friends = await _notificationScopeService.GetNotifiedUsers(userId); var friends = await _scopeService.GetNotifiedUsers(userId);
foreach (var recipient in friends) foreach (var recipient in friends)
{ {
@@ -51,14 +55,33 @@ public class PresenceHub : Hub
await base.OnConnectedAsync(); await base.OnConnectedAsync();
} }
public override async Task OnDisconnectedAsync(Exception? exception) public override async Task OnDisconnectedAsync(Exception exception)
{ {
var userId = _userAccessor.GetUserId(Context, true); var userId = _userAccessor.GetUserId(Context, true);
if (userId == Guid.Empty) return;
if (userId != Guid.Empty)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString());
_logger.LogInformation(
"User {UserId} disconnected with ConnectionId {ConnectionId} and removed from their group", userId,
Context.ConnectionId);
}
else
{
_logger.LogInformation(
"User disconnected with no exception and invalid UserID claim. ConnectionId: {ConnectionId}",
Context.ConnectionId);
return;
}
_onlineUserStore.SetOfflineUser(userId); _onlineUserStore.SetOfflineUser(userId);
var friends = await _notificationScopeService.GetNotifiedUsers(userId); // Updating was online
var user = await _users.FindByIdAsync(userId);
user.WasOnline = DateTime.UtcNow;
await _users.UpdateAsync(user);
var friends = await _scopeService.GetNotifiedUsers(userId);
foreach (var recipient in friends) foreach (var recipient in friends)
{ {