using System.Security.Claims; using ColaFlow.API.Hubs; using ColaFlow.API.Tests.Helpers; using FluentAssertions; using Microsoft.AspNetCore.SignalR; using Moq; namespace ColaFlow.API.Tests.Hubs; public class NotificationHubTests { private readonly Mock _mockClients; private readonly Mock _mockContext; private readonly Mock _mockCallerProxy; private readonly NotificationHub _hub; private readonly Guid _userId; private readonly Guid _tenantId; public NotificationHubTests() { _mockClients = new Mock(); _mockContext = new Mock(); _mockCallerProxy = new Mock(); _userId = Guid.NewGuid(); _tenantId = Guid.NewGuid(); // Setup context with valid claims var claims = new[] { new Claim("sub", _userId.ToString()), new Claim("tenant_id", _tenantId.ToString()) }; _mockContext.Setup(c => c.User).Returns(new ClaimsPrincipal(new ClaimsIdentity(claims))); _mockContext.Setup(c => c.ConnectionId).Returns("test-connection-id"); // Setup clients mock _mockClients.Setup(c => c.Caller).Returns(_mockCallerProxy.Object); _hub = new NotificationHub { Clients = _mockClients.Object, Context = _mockContext.Object }; } [Fact] public async Task MarkAsRead_WithValidNotificationId_SendsNotificationReadToCaller() { // Arrange var notificationId = Guid.NewGuid(); // Act await _hub.MarkAsRead(notificationId); // Assert _mockClients.Verify(c => c.Caller, Times.Once); _mockCallerProxy.Verify(p => p.SendCoreAsync( "NotificationRead", It.Is(args => args.Length == 1), default), Times.Once); } [Fact] public async Task MarkAsRead_ContainsCorrectNotificationId() { // Arrange var notificationId = Guid.NewGuid(); object? capturedData = null; _mockCallerProxy .Setup(p => p.SendCoreAsync("NotificationRead", It.IsAny(), default)) .Callback((method, args, ct) => { capturedData = args[0]; }) .Returns(Task.CompletedTask); // Act await _hub.MarkAsRead(notificationId); // Assert capturedData.Should().NotBeNull(); TestHelpers.GetPropertyValue(capturedData!, "NotificationId").Should().Be(notificationId); } [Fact] public async Task MarkAsRead_ContainsReadAtTimestamp() { // Arrange var notificationId = Guid.NewGuid(); object? capturedData = null; _mockCallerProxy .Setup(p => p.SendCoreAsync("NotificationRead", It.IsAny(), default)) .Callback((method, args, ct) => { capturedData = args[0]; }) .Returns(Task.CompletedTask); // Act await _hub.MarkAsRead(notificationId); // Assert capturedData.Should().NotBeNull(); TestHelpers.GetPropertyValue(capturedData!, "ReadAt").Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5)); } [Fact] public async Task MarkAsRead_MultipleNotifications_SendsMultipleEvents() { // Arrange var notificationId1 = Guid.NewGuid(); var notificationId2 = Guid.NewGuid(); var notificationId3 = Guid.NewGuid(); // Act await _hub.MarkAsRead(notificationId1); await _hub.MarkAsRead(notificationId2); await _hub.MarkAsRead(notificationId3); // Assert _mockCallerProxy.Verify(p => p.SendCoreAsync( "NotificationRead", It.IsAny(), default), Times.Exactly(3)); } [Fact] public async Task MarkAsRead_OnlySendsToCaller_NotOtherClients() { // Arrange var notificationId = Guid.NewGuid(); var mockOthersProxy = new Mock(); _mockClients.Setup(c => c.Others).Returns(mockOthersProxy.Object); // Act await _hub.MarkAsRead(notificationId); // Assert _mockCallerProxy.Verify(p => p.SendCoreAsync( "NotificationRead", It.IsAny(), default), Times.Once); mockOthersProxy.Verify(p => p.SendCoreAsync( It.IsAny(), It.IsAny(), default), Times.Never); } [Fact] public void MarkAsRead_WithMissingUserId_ThrowsUnauthorizedException() { // Arrange var notificationId = Guid.NewGuid(); var claims = new[] { new Claim("tenant_id", _tenantId.ToString()) }; _mockContext.Setup(c => c.User).Returns(new ClaimsPrincipal(new ClaimsIdentity(claims))); var hubWithoutUserId = new NotificationHub { Clients = _mockClients.Object, Context = _mockContext.Object }; // Act & Assert var act = async () => await hubWithoutUserId.MarkAsRead(notificationId); act.Should().ThrowAsync() .WithMessage("User ID not found in token"); } [Fact] public async Task MarkAsRead_EmptyGuid_StillProcesses() { // Arrange var emptyGuid = Guid.Empty; // Act var act = async () => await _hub.MarkAsRead(emptyGuid); // Assert await act.Should().NotThrowAsync(); _mockCallerProxy.Verify(p => p.SendCoreAsync( "NotificationRead", It.Is(args => args.Length == 1), default), Times.Once); } }