diff --git a/Govor.API.Tests/Common/SignalR/Helpers/HubUserAccessorTests.cs b/Govor.API.Tests/Common/SignalR/Helpers/HubUserAccessorTests.cs new file mode 100644 index 0000000..698999f --- /dev/null +++ b/Govor.API.Tests/Common/SignalR/Helpers/HubUserAccessorTests.cs @@ -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> _loggerMock; + + [SetUp] + public void SetUp() + { + _loggerMock = new Mock>(); + _accessor = new HubUserAccessor(_loggerMock.Object); + } + + private HubCallerContext CreateContextWithClaims(params Claim[] claims) + { + var principal = new ClaimsPrincipal(new ClaimsIdentity(claims)); + var contextMock = new Mock(); + 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(() => _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(() => _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(); + 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(); + contextMock.Setup(c => c.User).Returns((ClaimsPrincipal?)null); + // Act & Assert + Assert.Throws(() => _accessor.GetUserId(contextMock.Object, suppressException: false)); + } +} \ No newline at end of file diff --git a/Govor.API.Tests/IntegrationTests/Hubs/ChatsHubTests.cs b/Govor.API.Tests/IntegrationTests/Hubs/ChatsHubTests.cs index 8b4264c..a57b78c 100644 --- a/Govor.API.Tests/IntegrationTests/Hubs/ChatsHubTests.cs +++ b/Govor.API.Tests/IntegrationTests/Hubs/ChatsHubTests.cs @@ -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> _loggerMock; private Mock _messageServiceMock; private Mock _userGroupsServiceMock; + private Mock _hubUserAccessorMock; private Fixture _fixture; private ChatsHub _chatsHub; @@ -26,11 +28,13 @@ public class ChatsHubTests _messageServiceMock = new Mock(); _userGroupsServiceMock = new Mock(); _loggerMock = new Mock>(); + _hubUserAccessorMock = new Mock(); _chatsHub = new ChatsHub( _loggerMock.Object, _messageServiceMock.Object, - _userGroupsServiceMock.Object + _userGroupsServiceMock.Object, + _hubUserAccessorMock.Object ); } diff --git a/Govor.API.Tests/IntegrationTests/Hubs/FriendsHubTests.cs b/Govor.API.Tests/IntegrationTests/Hubs/FriendsHubTests.cs index 57010f7..ce1963c 100644 --- a/Govor.API.Tests/IntegrationTests/Hubs/FriendsHubTests.cs +++ b/Govor.API.Tests/IntegrationTests/Hubs/FriendsHubTests.cs @@ -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 _friendRequestServiceMock = null!; - private Mock _currentUserServiceMock = null!; + private Mock _currentUserServiceMock = null!; private Mock _clientsMock = null!; private Mock _clientProxyMock = null!; private Mock> _loggerMock = null!; @@ -35,19 +36,23 @@ public class FriendsHubTests _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); _friendRequestServiceMock = new Mock(); - _currentUserServiceMock = new Mock(); + _currentUserServiceMock = new Mock(); _clientsMock = new Mock(); _clientProxyMock = new Mock(); _loggerMock = new Mock>(); _mapperMock = new Mock(); - _currentUserServiceMock.Setup(x => x.GetCurrentUserId()).Returns(_userId); + _currentUserServiceMock.Setup(x => x.GetUserId( + It.IsAny(), + It.IsAny())) + .Returns(_userId); + _clientsMock.Setup(c => c.Group(It.IsAny())).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(), It.IsAny())) .Throws(new UnauthorizedAccessException("userId claim is missing or invalid")); // Act diff --git a/Govor.API/Extensions/AddOptionExtensions.cs b/Govor.API/Common/Extensions/AddOptionExtensions.cs similarity index 91% rename from Govor.API/Extensions/AddOptionExtensions.cs rename to Govor.API/Common/Extensions/AddOptionExtensions.cs index aa0951c..060de2b 100644 --- a/Govor.API/Extensions/AddOptionExtensions.cs +++ b/Govor.API/Common/Extensions/AddOptionExtensions.cs @@ -1,6 +1,6 @@ using Govor.Application.Services.Authentication; -namespace Govor.API.Extensions; +namespace Govor.API.Common.Extensions; public static class AddOptionExtensions { diff --git a/Govor.API/Extensions/ConfigurationProgramExtensions.cs b/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs similarity index 97% rename from Govor.API/Extensions/ConfigurationProgramExtensions.cs rename to Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs index be0667d..6a1602a 100644 --- a/Govor.API/Extensions/ConfigurationProgramExtensions.cs +++ b/Govor.API/Common/Extensions/ConfigurationProgramExtensions.cs @@ -1,3 +1,5 @@ +using Govor.API.Common.Mapping; +using Govor.API.Common.SignalR.Helpers; using Govor.Application.Infrastructure.AdminsStuff; using Govor.Application.Infrastructure.Extensions; using Govor.Application.Infrastructure.Validators; @@ -34,7 +36,7 @@ using Govor.Data; using Govor.Data.Repositories; using Microsoft.EntityFrameworkCore; -namespace Govor.API.Extensions; +namespace Govor.API.Common.Extensions; public static class ConfigurationProgramExtensions { @@ -83,6 +85,8 @@ public static class ConfigurationProgramExtensions // Auto Mapper services.AddAutoMapper(typeof(MappingProfile)); + + services.AddScoped(); } public static void AddRepositories(this IServiceCollection services) diff --git a/Govor.API/Extensions/ConfigurationSignalR.cs b/Govor.API/Common/Extensions/ConfigurationSignalR.cs similarity index 88% rename from Govor.API/Extensions/ConfigurationSignalR.cs rename to Govor.API/Common/Extensions/ConfigurationSignalR.cs index 69cbe0d..935eb6c 100644 --- a/Govor.API/Extensions/ConfigurationSignalR.cs +++ b/Govor.API/Common/Extensions/ConfigurationSignalR.cs @@ -1,7 +1,7 @@ using Govor.API.Filters; using Microsoft.AspNetCore.SignalR; -namespace Govor.API.Extensions; +namespace Govor.API.Common.Extensions; public static class ConfigurationSignalR { diff --git a/Govor.API/Extensions/ConfiguratorLoggerExtensions.cs b/Govor.API/Common/Extensions/ConfiguratorLoggerExtensions.cs similarity index 92% rename from Govor.API/Extensions/ConfiguratorLoggerExtensions.cs rename to Govor.API/Common/Extensions/ConfiguratorLoggerExtensions.cs index 1ef16b4..a8ccddc 100644 --- a/Govor.API/Extensions/ConfiguratorLoggerExtensions.cs +++ b/Govor.API/Common/Extensions/ConfiguratorLoggerExtensions.cs @@ -1,6 +1,6 @@ using Serilog; -namespace Govor.API.Extensions; +namespace Govor.API.Common.Extensions; public static class ConfiguratorLoggerExtensions { diff --git a/Govor.API/Extensions/MappingProfile.cs b/Govor.API/Common/Mapping/MappingProfile.cs similarity index 75% rename from Govor.API/Extensions/MappingProfile.cs rename to Govor.API/Common/Mapping/MappingProfile.cs index 69045d6..5b15e9e 100644 --- a/Govor.API/Extensions/MappingProfile.cs +++ b/Govor.API/Common/Mapping/MappingProfile.cs @@ -1,11 +1,12 @@ using AutoMapper; +using Govor.API.Extensions.Mapping; using Govor.Contracts.DTOs; using Govor.Contracts.Responses; using Govor.Core.Models; using Govor.Core.Models.Messages; using Govor.Core.Models.Users; -namespace Govor.API.Extensions; +namespace Govor.API.Common.Mapping; public class MappingProfile : Profile { @@ -16,7 +17,9 @@ public class MappingProfile : Profile CreateMap(); CreateMap(); - CreateMap(); + CreateMap() + .AfterMap(); + CreateMap(); } } \ No newline at end of file diff --git a/Govor.API/Common/Mapping/UserToUserDtoMappingAction.cs b/Govor.API/Common/Mapping/UserToUserDtoMappingAction.cs new file mode 100644 index 0000000..c915fb3 --- /dev/null +++ b/Govor.API/Common/Mapping/UserToUserDtoMappingAction.cs @@ -0,0 +1,21 @@ +using AutoMapper; +using Govor.Application.Interfaces.UserOnlineStatus; +using Govor.Contracts.DTOs; +using Govor.Core.Models.Users; + +namespace Govor.API.Extensions.Mapping; + +public class UserToUserDtoMappingAction : IMappingAction +{ + private readonly IOnlineUserStore _onlineUserStore; + + public UserToUserDtoMappingAction(IOnlineUserStore onlineUserStore) + { + _onlineUserStore = onlineUserStore; + } + + public void Process(User source, UserDto destination, ResolutionContext context) + { + destination.IsOnline = _onlineUserStore.IsOnline(source.Id); + } +} \ No newline at end of file diff --git a/Govor.API/Common/SignalR/Helpers/HubUserAccessor.cs b/Govor.API/Common/SignalR/Helpers/HubUserAccessor.cs new file mode 100644 index 0000000..2cf2ece --- /dev/null +++ b/Govor.API/Common/SignalR/Helpers/HubUserAccessor.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.SignalR; + +namespace Govor.API.Common.SignalR.Helpers; + +public class HubUserAccessor : IHubUserAccessor +{ + private readonly ILogger _logger; + + public HubUserAccessor(ILogger logger) + { + _logger = logger; + } + + public Guid GetUserId(HubCallerContext context, bool suppressException = false) + { + var userIdClaim = context.User?.FindFirst("userId")?.Value; + if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId)) + { + if (!suppressException) + { + _logger.LogError("Could not retrieve sender userId. Claim was: {UserIDClaim}", userIdClaim); + throw new UnauthorizedAccessException("userID claim is missing or invalid."); + } + + return Guid.Empty; + } + + return userId; + } +} \ No newline at end of file diff --git a/Govor.API/Common/SignalR/Helpers/IHubUserAccessor.cs b/Govor.API/Common/SignalR/Helpers/IHubUserAccessor.cs new file mode 100644 index 0000000..11f7604 --- /dev/null +++ b/Govor.API/Common/SignalR/Helpers/IHubUserAccessor.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.SignalR; + +namespace Govor.API.Common.SignalR.Helpers; + +public interface IHubUserAccessor +{ + Guid GetUserId(HubCallerContext context, bool suppressException = false); +} diff --git a/Govor.API/Hubs/ChatsHub.cs b/Govor.API/Hubs/ChatsHub.cs index 2f50ea8..127460f 100644 --- a/Govor.API/Hubs/ChatsHub.cs +++ b/Govor.API/Hubs/ChatsHub.cs @@ -1,3 +1,4 @@ +using Govor.API.Common.SignalR.Helpers; using Govor.Application.Exceptions.VerifyFriendship; using Govor.Application.Interfaces; using Govor.Application.Interfaces.Messages; @@ -16,17 +17,19 @@ public class ChatsHub : Hub private readonly ILogger _logger; private readonly IMessageCommandService _messageCommandService; private readonly IUserGroupsService _userService; + private readonly IHubUserAccessor _userAccessor; - public ChatsHub(ILogger logger, IMessageCommandService messageCommandService, IUserGroupsService userService) + public ChatsHub(ILogger logger, IMessageCommandService messageCommandService, IUserGroupsService userService, IHubUserAccessor userAccessor) { _logger = logger; _messageCommandService = messageCommandService; _userService = userService; + _userAccessor = userAccessor; } public override async Task OnConnectedAsync() { - var userId = GetUserId(); + var userId = _userAccessor.GetUserId(Context); if (userId == Guid.Empty) { _logger.LogWarning("User connected with invalid UserID claim."); @@ -50,8 +53,7 @@ public class ChatsHub : Hub public override async Task OnDisconnectedAsync(Exception? exception) { - var userId = - GetUserId(suppressException: true); + var userId = _userAccessor.GetUserId(Context, true); if (userId != Guid.Empty) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId.ToString()); @@ -85,7 +87,7 @@ public class ChatsHub : Hub public async Task> Send(MessageRequest request) { - var senderId = GetUserId(); + var senderId= _userAccessor.GetUserId(Context); if (string.IsNullOrWhiteSpace(request.EncryptedContent) && (request.MediaAttachments == null || !request.MediaAttachments.Any())) @@ -144,7 +146,7 @@ public class ChatsHub : Hub public async Task> Remove(RemoveMessageRequest request) { - var removerId = GetUserId(); + var removerId = _userAccessor.GetUserId(Context); _logger.LogInformation("Removing message {MessageId} by user {RemoverId}", request.MessageId, removerId); try @@ -184,7 +186,7 @@ public class ChatsHub : Hub public async Task> Edit(EditMessageRequest request) { - var editor = GetUserId(); + var editor = _userAccessor.GetUserId(Context); _logger.LogInformation("Editing message {MessageId} by user {EditorId}", request.MessageId, editor); var editMessageParam = new EditMessage(editor, @@ -303,22 +305,4 @@ public class ChatsHub : Hub _logger.LogWarning(ex, "{Msg}: {UserId} -> {TargetId}", msg, userId, targetId); return HubResult.NotFound("Message not found."); } - - - private Guid GetUserId(bool suppressException = false) - { - var userIdClaim = Context.User?.FindFirst("userId")?.Value; - if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId)) - { - if (!suppressException) - { - _logger.LogError("Could not retrieve sender userId. Claim was: {UserIDClaim}", userIdClaim); - throw new UnauthorizedAccessException("userID claim is missing or invalid."); - } - - return Guid.Empty; - } - - return userId; - } } \ No newline at end of file diff --git a/Govor.API/Hubs/FriendsHub.cs b/Govor.API/Hubs/FriendsHub.cs index 3fa025e..7aa9564 100644 --- a/Govor.API/Hubs/FriendsHub.cs +++ b/Govor.API/Hubs/FriendsHub.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using AutoMapper; +using Govor.API.Common.SignalR.Helpers; using Govor.Application.Exceptions.FriendsService; using Govor.Application.Interfaces.Friends; using Govor.Application.Interfaces.Infrastructure.Extensions; @@ -13,23 +14,24 @@ public class FriendsHub : Hub { private readonly ILogger _logger; private readonly IFriendRequestCommandService _friendRequestService; - private readonly ICurrentUserService _currentUserService; + private readonly IHubUserAccessor _userAccessor; private readonly IMapper _mapper; - - public FriendsHub(IFriendRequestCommandService friendRequestService, - ICurrentUserService currentUserService, + + public FriendsHub( ILogger logger, + IFriendRequestCommandService friendRequestService, + IHubUserAccessor userAccessor, IMapper mapper) { - _friendRequestService = friendRequestService; - _currentUserService = currentUserService; _logger = logger; + _friendRequestService = friendRequestService; + _userAccessor = userAccessor; _mapper = mapper; } - + public override async Task OnConnectedAsync() { - var userId = GetUserId(); + var userId = _userAccessor.GetUserId(Context); if (userId == Guid.Empty) { _logger.LogWarning("User connected with invalid UserID claim."); @@ -46,8 +48,7 @@ public class FriendsHub : Hub } public override async Task OnDisconnectedAsync(Exception? exception) { - var userId = - GetUserId(suppressException: true); + var userId = _userAccessor.GetUserId(Context, true); if (userId != Guid.Empty) { // Remove user from their own group @@ -76,7 +77,7 @@ public class FriendsHub : Hub { try { - var userId = _currentUserService.GetCurrentUserId(); + var userId = _userAccessor.GetUserId(Context); var friendship = await _friendRequestService.SendAsync(userId, targetUserId); await Clients.Group(targetUserId.ToString()) @@ -111,7 +112,7 @@ public class FriendsHub : Hub { try { - var userId = _currentUserService.GetCurrentUserId(); + var userId = _userAccessor.GetUserId(Context); var friendship = await _friendRequestService.AcceptAsync(friendshipId, userId); await Clients.Group(userId.ToString()) .SendAsync("FriendRequestAccepted", _mapper.Map(friendship)); @@ -140,7 +141,7 @@ public class FriendsHub : Hub { try { - var userId = _currentUserService.GetCurrentUserId(); + var userId = _userAccessor.GetUserId(Context); var friendship = await _friendRequestService.RejectAsync(friendshipId, userId); await Clients.Group(userId.ToString()) .SendAsync("FriendRequestRejected", _mapper.Map(friendship)); @@ -164,21 +165,4 @@ public class FriendsHub : Hub return HubResult.Error("Unexpected error! Please try later!"); } } - - private Guid GetUserId(bool suppressException = false) - { - var userIdClaim = Context.User?.FindFirst("userId")?.Value; - if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId)) - { - if (!suppressException) - { - _logger.LogError("Could not retrieve sender userId. Claim was: {UserIDClaim}", userIdClaim); - throw new UnauthorizedAccessException("userID claim is missing or invalid."); - } - - return Guid.Empty; - } - - return userId; - } } \ No newline at end of file diff --git a/Govor.API/Hubs/PresenceHub.cs b/Govor.API/Hubs/PresenceHub.cs index fc23170..76843bc 100644 --- a/Govor.API/Hubs/PresenceHub.cs +++ b/Govor.API/Hubs/PresenceHub.cs @@ -1,5 +1,4 @@ -using Govor.Application.Interfaces; -using Govor.Application.Interfaces.Infrastructure.Extensions; +using Govor.API.Common.SignalR.Helpers; using Govor.Application.Interfaces.UserOnlineStatus; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components; @@ -14,17 +13,23 @@ public class PresenceHub : Hub private readonly ILogger _logger; private readonly IUserNotificationScopeService _notificationScopeService; private readonly IOnlineUserStore _onlineUserStore; - - public PresenceHub(ILogger logger, IUserNotificationScopeService notificationScopeService, IOnlineUserStore onlineUserStore) + private readonly IHubUserAccessor _userAccessor; + + public PresenceHub( + ILogger logger, + IUserNotificationScopeService notificationScopeService, + IOnlineUserStore onlineUserStore, + IHubUserAccessor userAccessor) { _logger = logger; _notificationScopeService = notificationScopeService; _onlineUserStore = onlineUserStore; + _userAccessor = userAccessor; } public override async Task OnConnectedAsync() { - var userId = GetUserId(); + var userId = _userAccessor.GetUserId(Context); if (userId == Guid.Empty) { _logger.LogWarning("User connected with invalid UserID claim."); @@ -48,7 +53,7 @@ public class PresenceHub : Hub public override async Task OnDisconnectedAsync(Exception? exception) { - var userId = GetUserId(); + var userId = _userAccessor.GetUserId(Context, true); if (userId == Guid.Empty) return; _onlineUserStore.SetOfflineUser(userId); @@ -63,21 +68,4 @@ public class PresenceHub : Hub await base.OnDisconnectedAsync(exception); } - - private Guid GetUserId(bool suppressException = false) - { - var userIdClaim = Context.User?.FindFirst("userId")?.Value; - if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId)) - { - if (!suppressException) - { - _logger.LogError("Could not retrieve sender userId. Claim was: {UserIDClaim}", userIdClaim); - throw new UnauthorizedAccessException("userId claim is missing or invalid."); - } - - return Guid.Empty; - } - - return userId; - } } \ No newline at end of file diff --git a/Govor.API/Program.cs b/Govor.API/Program.cs index 2e1886e..935a705 100644 --- a/Govor.API/Program.cs +++ b/Govor.API/Program.cs @@ -1,5 +1,5 @@ using System.Text; -using Govor.API.Extensions; +using Govor.API.Common.Extensions; using Govor.API.Hubs; using Govor.Application.Services.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; diff --git a/Govor.Application.Tests/Services/UserOnlineStatus/OnlineUserStoreTests.cs b/Govor.Application.Tests/Services/UserOnlineStatus/OnlineUserStoreTests.cs new file mode 100644 index 0000000..f97a677 --- /dev/null +++ b/Govor.Application.Tests/Services/UserOnlineStatus/OnlineUserStoreTests.cs @@ -0,0 +1,82 @@ +using Govor.Application.Interfaces.UserOnlineStatus; +using Govor.Application.Services.UserOnlineStatus; + +namespace Govor.Application.Tests.Services.UserOnlineStatus; + +[TestFixture] +public class OnlineUserStoreTests +{ + private IOnlineUserStore _store; + private Guid _userId1; + private Guid _userId2; + + [SetUp] + public void Setup() + { + _store = new OnlineUserStore(); + _userId1 = Guid.NewGuid(); + _userId2 = Guid.NewGuid(); + } + + [Test] + public void SetOnlineUser_UserIsMarkedOnline() + { + // Act + _store.SetOnlineUser(_userId1); + + // Assert + Assert.That(_store.IsOnline(_userId1), Is.True); + } + + [Test] + public void SetOfflineUser_UserIsNoLongerOnline() + { + // Act + _store.SetOnlineUser(_userId1); + _store.SetOfflineUser(_userId1); + + // Assert + Assert.That(_store.IsOnline(_userId1), Is.False); + } + + [Test] + public void IsOnline_ReturnsFalse_ForUnknownUser() + { + // Act & Assert + Assert.That(_store.IsOnline(Guid.NewGuid()), Is.False); + } + + [Test] + public void GetAllOnlineUsers_ReturnsAllCurrentlyOnlineUsers() + { + // Arrange + _store.SetOnlineUser(_userId1); + _store.SetOnlineUser(_userId2); + + // Act + var onlineUsers = _store.GetAllOnlineUsers(); + + // Assert + Assert.That(onlineUsers, Is.EquivalentTo(new[] { _userId1, _userId2 })); + } + + [Test] + public void SetOnlineUser_Twice_DoesNotThrow() + { + // Act & Assert + Assert.DoesNotThrow(() => + { + _store.SetOnlineUser(_userId1); + _store.SetOnlineUser(_userId1); + }); + + Assert.That(_store.IsOnline(_userId1), Is.True); + } + + [Test] + public void SetOfflineUser_ForUnknownUser_DoesNotThrow() + { + // Act & Assert + Assert.DoesNotThrow(() => _store.SetOfflineUser(Guid.NewGuid())); + } +} \ No newline at end of file diff --git a/Govor.Application/Services/UserOnlineStatus/OnlineUserStore.cs b/Govor.Application/Services/UserOnlineStatus/OnlineUserStore.cs index a3813b9..2100472 100644 --- a/Govor.Application/Services/UserOnlineStatus/OnlineUserStore.cs +++ b/Govor.Application/Services/UserOnlineStatus/OnlineUserStore.cs @@ -1,26 +1,29 @@ +using System.Collections.Concurrent; using Govor.Application.Interfaces.UserOnlineStatus; namespace Govor.Application.Services.UserOnlineStatus; public class OnlineUserStore : IOnlineUserStore { + private readonly ConcurrentDictionary _onlineUsers = new(); + public void SetOnlineUser(Guid userId) { - throw new NotImplementedException(); + _onlineUsers[userId] = DateTime.UtcNow; } public void SetOfflineUser(Guid userId) { - throw new NotImplementedException(); + _onlineUsers.TryRemove(userId, out _); } public bool IsOnline(Guid userId) { - throw new NotImplementedException(); + return _onlineUsers.ContainsKey(userId); } public IReadOnlyCollection GetAllOnlineUsers() { - throw new NotImplementedException(); + return _onlineUsers.Keys.ToList(); } -} \ No newline at end of file +}