diff --git a/Govor.API.Tests/Hubs/PresenceHubTests.cs b/Govor.API.Tests/Hubs/PresenceHubTests.cs index 2a39aae..3a4df98 100644 --- a/Govor.API.Tests/Hubs/PresenceHubTests.cs +++ b/Govor.API.Tests/Hubs/PresenceHubTests.cs @@ -2,6 +2,8 @@ using AutoFixture; using Govor.API.Common.SignalR.Helpers; using Govor.API.Hubs; using Govor.Application.Interfaces.UserOnlineStatus; +using Govor.Core.Repositories.Users; +using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; using Moq; @@ -16,11 +18,89 @@ public class PresenceHubTests private Mock _mockUserNotification; private Mock _mockOnlineUserStore; private Mock _mockHubUserAccessor; + private Mock _mockUserRepository; + private Mock _clientsMock = null!; + private Mock _clientProxyMock = null!; + private Mock _mockContext = null!; + private Mock _groupManagerMock = null!; private Fixture _fixture; + private PresenceHub _hub; + private Guid _userId; [SetUp] public void SetUp() { + _fixture = new Fixture(); + _fixture.Behaviors.OfType().ToList().ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + _mockLogger = new Mock>(); + _mockUserNotification = new Mock(); + _mockOnlineUserStore = new Mock(); + _mockHubUserAccessor = new Mock(); + _mockUserRepository = new Mock(); + + _clientsMock = new Mock(); + _clientProxyMock = new Mock(); + + _userId = Guid.NewGuid(); + _mockHubUserAccessor.Setup(h => h.GetUserId( + It.IsAny(), + It.IsAny())) + .Returns(_userId); + + + _clientsMock.Setup(c => c.Group(It.IsAny())).Returns(_clientProxyMock.Object); + _mockContext = new Mock(); + _mockContext.Setup(c => c.ConnectionId).Returns("test-connection"); + + _groupManagerMock = new Mock(); + + _groupManagerMock.Setup(g => g.AddToGroupAsync(It.IsAny(), It.IsAny(), 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().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()), Times.Exactly(friendsIds.Count)); + + foreach (var friendId in friendsIds) + { + _clientProxyMock.Verify(proxy => + proxy.SendCoreAsync("UserOnline", + It.Is(args => args.Length == 1 && (Guid)args[0] == _userId), + It.IsAny()), + Times.Exactly(friendsIds.Count)); + } + } + } \ No newline at end of file diff --git a/Govor.API/Hubs/FriendsHub.cs b/Govor.API/Hubs/FriendsHub.cs index 50b8e2a..75cf871 100644 --- a/Govor.API/Hubs/FriendsHub.cs +++ b/Govor.API/Hubs/FriendsHub.cs @@ -49,7 +49,6 @@ public class FriendsHub : Hub var userId = _userAccessor.GetUserId(Context, true); if (userId != Guid.Empty) { - // Remove user from their own group await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString()); _logger.LogInformation( "User {UserId} disconnected with ConnectionId {ConnectionId} and removed from their group", userId, diff --git a/Govor.API/Hubs/PresenceHub.cs b/Govor.API/Hubs/PresenceHub.cs index 76843bc..990bf23 100644 --- a/Govor.API/Hubs/PresenceHub.cs +++ b/Govor.API/Hubs/PresenceHub.cs @@ -1,5 +1,6 @@ using Govor.API.Common.SignalR.Helpers; using Govor.Application.Interfaces.UserOnlineStatus; +using Govor.Core.Repositories.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.SignalR; @@ -11,18 +12,21 @@ namespace Govor.API.Hubs; public class PresenceHub : Hub { private readonly ILogger _logger; - private readonly IUserNotificationScopeService _notificationScopeService; + private readonly IUserNotificationScopeService _scopeService; private readonly IOnlineUserStore _onlineUserStore; private readonly IHubUserAccessor _userAccessor; - + private readonly IUsersRepository _users; + public PresenceHub( ILogger logger, - IUserNotificationScopeService notificationScopeService, + IUserNotificationScopeService scopeService, IOnlineUserStore onlineUserStore, - IHubUserAccessor userAccessor) + IHubUserAccessor userAccessor, + IUsersRepository users) { _logger = logger; - _notificationScopeService = notificationScopeService; + _users = users; + _scopeService = scopeService; _onlineUserStore = onlineUserStore; _userAccessor = userAccessor; } @@ -30,9 +34,9 @@ public class PresenceHub : Hub public override async Task OnConnectedAsync() { 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(); return; } @@ -40,7 +44,7 @@ public class PresenceHub : Hub _onlineUserStore.SetOnlineUser(userId); await Groups.AddToGroupAsync(Context.ConnectionId, userId.ToString()); - var friends = await _notificationScopeService.GetNotifiedUsers(userId); + var friends = await _scopeService.GetNotifiedUsers(userId); foreach (var recipient in friends) { @@ -51,14 +55,33 @@ public class PresenceHub : Hub await base.OnConnectedAsync(); } - public override async Task OnDisconnectedAsync(Exception? exception) + public override async Task OnDisconnectedAsync(Exception exception) { 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); - - 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) {