using ColaFlow.API.Hubs; using ColaFlow.API.Services; using ColaFlow.API.Tests.Helpers; using FluentAssertions; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; using Moq; namespace ColaFlow.API.Tests.Services; public class RealtimeNotificationServiceTests { private readonly Mock> _mockProjectHubContext; private readonly Mock> _mockNotificationHubContext; private readonly Mock> _mockLogger; private readonly Mock _mockProjectClients; private readonly Mock _mockNotificationClients; private readonly Mock _mockClientProxy; private readonly RealtimeNotificationService _service; public RealtimeNotificationServiceTests() { _mockProjectHubContext = new Mock>(); _mockNotificationHubContext = new Mock>(); _mockLogger = new Mock>(); _mockProjectClients = new Mock(); _mockNotificationClients = new Mock(); _mockClientProxy = new Mock(); _mockProjectHubContext.Setup(h => h.Clients).Returns(_mockProjectClients.Object); _mockNotificationHubContext.Setup(h => h.Clients).Returns(_mockNotificationClients.Object); _mockProjectClients.Setup(c => c.Group(It.IsAny())).Returns(_mockClientProxy.Object); _mockNotificationClients.Setup(c => c.Group(It.IsAny())).Returns(_mockClientProxy.Object); _mockNotificationClients.Setup(c => c.User(It.IsAny())).Returns(_mockClientProxy.Object); _service = new RealtimeNotificationService( _mockProjectHubContext.Object, _mockNotificationHubContext.Object, _mockLogger.Object); } [Fact] public async Task NotifyProjectCreated_SendsToTenantGroup() { // Arrange var tenantId = Guid.NewGuid(); var projectId = Guid.NewGuid(); var project = new { Id = projectId, Name = "Test Project" }; // Act await _service.NotifyProjectCreated(tenantId, projectId, project); // Assert _mockProjectClients.Verify(c => c.Group($"tenant-{tenantId}"), Times.Once); _mockClientProxy.Verify(p => p.SendCoreAsync( "ProjectCreated", It.Is(args => args.Length == 1 && args[0] == project), default), Times.Once); } [Fact] public async Task NotifyProjectUpdated_SendsToBothProjectAndTenantGroups() { // Arrange var tenantId = Guid.NewGuid(); var projectId = Guid.NewGuid(); var project = new { Id = projectId, Name = "Updated Project" }; // Act await _service.NotifyProjectUpdated(tenantId, projectId, project); // Assert _mockProjectClients.Verify(c => c.Group($"project-{projectId}"), Times.Once); _mockProjectClients.Verify(c => c.Group($"tenant-{tenantId}"), Times.Once); _mockClientProxy.Verify(p => p.SendCoreAsync( "ProjectUpdated", It.Is(args => args.Length == 1 && args[0] == project), default), Times.Exactly(2)); } [Fact] public async Task NotifyProjectArchived_SendsToBothGroups() { // Arrange var tenantId = Guid.NewGuid(); var projectId = Guid.NewGuid(); // Act await _service.NotifyProjectArchived(tenantId, projectId); // Assert _mockProjectClients.Verify(c => c.Group($"project-{projectId}"), Times.Once); _mockProjectClients.Verify(c => c.Group($"tenant-{tenantId}"), Times.Once); _mockClientProxy.Verify(p => p.SendCoreAsync( "ProjectArchived", It.Is(args => args.Length == 1), default), Times.Exactly(2)); } [Fact] public async Task NotifyProjectArchived_ContainsCorrectProjectId() { // Arrange var tenantId = Guid.NewGuid(); var projectId = Guid.NewGuid(); object? capturedData = null; _mockClientProxy .Setup(p => p.SendCoreAsync("ProjectArchived", It.IsAny(), default)) .Callback((method, args, ct) => { capturedData = args[0]; }) .Returns(Task.CompletedTask); // Act await _service.NotifyProjectArchived(tenantId, projectId); // Assert capturedData.Should().NotBeNull(); TestHelpers.GetPropertyValue(capturedData!, "ProjectId").Should().Be(projectId); } [Fact] public async Task NotifyProjectUpdate_SendsToProjectGroup() { // Arrange var tenantId = Guid.NewGuid(); var projectId = Guid.NewGuid(); var updateData = new { Field = "Status", Value = "Active" }; // Act await _service.NotifyProjectUpdate(tenantId, projectId, updateData); // Assert _mockProjectClients.Verify(c => c.Group($"project-{projectId}"), Times.Once); _mockClientProxy.Verify(p => p.SendCoreAsync( "ProjectUpdated", It.Is(args => args.Length == 1 && args[0] == updateData), default), Times.Once); } [Fact] public async Task NotifyIssueCreated_SendsToProjectGroup() { // Arrange var tenantId = Guid.NewGuid(); var projectId = Guid.NewGuid(); var issue = new { Id = Guid.NewGuid(), Title = "New Issue" }; // Act await _service.NotifyIssueCreated(tenantId, projectId, issue); // Assert _mockProjectClients.Verify(c => c.Group($"project-{projectId}"), Times.Once); _mockClientProxy.Verify(p => p.SendCoreAsync( "IssueCreated", It.Is(args => args.Length == 1 && args[0] == issue), default), Times.Once); } [Fact] public async Task NotifyIssueUpdated_SendsToProjectGroup() { // Arrange var tenantId = Guid.NewGuid(); var projectId = Guid.NewGuid(); var issue = new { Id = Guid.NewGuid(), Title = "Updated Issue" }; // Act await _service.NotifyIssueUpdated(tenantId, projectId, issue); // Assert _mockProjectClients.Verify(c => c.Group($"project-{projectId}"), Times.Once); _mockClientProxy.Verify(p => p.SendCoreAsync( "IssueUpdated", It.Is(args => args.Length == 1 && args[0] == issue), default), Times.Once); } [Fact] public async Task NotifyIssueDeleted_SendsToProjectGroup() { // Arrange var tenantId = Guid.NewGuid(); var projectId = Guid.NewGuid(); var issueId = Guid.NewGuid(); // Act await _service.NotifyIssueDeleted(tenantId, projectId, issueId); // Assert _mockProjectClients.Verify(c => c.Group($"project-{projectId}"), Times.Once); _mockClientProxy.Verify(p => p.SendCoreAsync( "IssueDeleted", It.Is(args => args.Length == 1), default), Times.Once); } [Fact] public async Task NotifyIssueDeleted_ContainsCorrectIssueId() { // Arrange var tenantId = Guid.NewGuid(); var projectId = Guid.NewGuid(); var issueId = Guid.NewGuid(); object? capturedData = null; _mockClientProxy .Setup(p => p.SendCoreAsync("IssueDeleted", It.IsAny(), default)) .Callback((method, args, ct) => { capturedData = args[0]; }) .Returns(Task.CompletedTask); // Act await _service.NotifyIssueDeleted(tenantId, projectId, issueId); // Assert capturedData.Should().NotBeNull(); TestHelpers.GetPropertyValue(capturedData!, "IssueId").Should().Be(issueId); } [Fact] public async Task NotifyIssueStatusChanged_SendsWithCorrectData() { // Arrange var tenantId = Guid.NewGuid(); var projectId = Guid.NewGuid(); var issueId = Guid.NewGuid(); var oldStatus = "To Do"; var newStatus = "In Progress"; object? capturedData = null; _mockClientProxy .Setup(p => p.SendCoreAsync("IssueStatusChanged", It.IsAny(), default)) .Callback((method, args, ct) => { capturedData = args[0]; }) .Returns(Task.CompletedTask); // Act await _service.NotifyIssueStatusChanged(tenantId, projectId, issueId, oldStatus, newStatus); // Assert capturedData.Should().NotBeNull(); TestHelpers.GetPropertyValue(capturedData!, "IssueId").Should().Be(issueId); TestHelpers.GetPropertyValue(capturedData!, "OldStatus").Should().Be(oldStatus); TestHelpers.GetPropertyValue(capturedData!, "NewStatus").Should().Be(newStatus); TestHelpers.GetPropertyValue(capturedData!, "ChangedAt").Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5)); } [Fact] public async Task NotifyIssueStatusChanged_SendsToProjectGroup() { // Arrange var tenantId = Guid.NewGuid(); var projectId = Guid.NewGuid(); var issueId = Guid.NewGuid(); // Act await _service.NotifyIssueStatusChanged(tenantId, projectId, issueId, "To Do", "Done"); // Assert _mockProjectClients.Verify(c => c.Group($"project-{projectId}"), Times.Once); _mockClientProxy.Verify(p => p.SendCoreAsync( "IssueStatusChanged", It.IsAny(), default), Times.Once); } [Fact] public async Task NotifyUser_SendsToSpecificUser() { // Arrange var userId = Guid.NewGuid(); var message = "Test notification"; var type = "info"; // Act await _service.NotifyUser(userId, message, type); // Assert _mockNotificationClients.Verify(c => c.User(userId.ToString()), Times.Once); _mockClientProxy.Verify(p => p.SendCoreAsync( "Notification", It.Is(args => args.Length == 1), default), Times.Once); } [Fact] public async Task NotifyUser_ContainsCorrectMessageData() { // Arrange var userId = Guid.NewGuid(); var message = "Important update"; var type = "warning"; object? capturedData = null; _mockClientProxy .Setup(p => p.SendCoreAsync("Notification", It.IsAny(), default)) .Callback((method, args, ct) => { capturedData = args[0]; }) .Returns(Task.CompletedTask); // Act await _service.NotifyUser(userId, message, type); // Assert capturedData.Should().NotBeNull(); TestHelpers.GetPropertyValue(capturedData!, "Message").Should().Be(message); TestHelpers.GetPropertyValue(capturedData!, "Type").Should().Be(type); TestHelpers.GetPropertyValue(capturedData!, "Timestamp").Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5)); } [Fact] public async Task NotifyUser_WithDefaultType_UsesInfo() { // Arrange var userId = Guid.NewGuid(); var message = "Test"; object? capturedData = null; _mockClientProxy .Setup(p => p.SendCoreAsync("Notification", It.IsAny(), default)) .Callback((method, args, ct) => { capturedData = args[0]; }) .Returns(Task.CompletedTask); // Act await _service.NotifyUser(userId, message); // Assert capturedData.Should().NotBeNull(); TestHelpers.GetPropertyValue(capturedData!, "Type").Should().Be("info"); } [Fact] public async Task NotifyUsersInTenant_SendsToTenantGroup() { // Arrange var tenantId = Guid.NewGuid(); var message = "Tenant announcement"; var type = "info"; // Act await _service.NotifyUsersInTenant(tenantId, message, type); // Assert _mockNotificationClients.Verify(c => c.Group($"tenant-{tenantId}"), Times.Once); _mockClientProxy.Verify(p => p.SendCoreAsync( "Notification", It.Is(args => args.Length == 1), default), Times.Once); } [Fact] public async Task NotifyUsersInTenant_ContainsCorrectData() { // Arrange var tenantId = Guid.NewGuid(); var message = "System maintenance scheduled"; var type = "warning"; object? capturedData = null; _mockClientProxy .Setup(p => p.SendCoreAsync("Notification", It.IsAny(), default)) .Callback((method, args, ct) => { capturedData = args[0]; }) .Returns(Task.CompletedTask); // Act await _service.NotifyUsersInTenant(tenantId, message, type); // Assert capturedData.Should().NotBeNull(); TestHelpers.GetPropertyValue(capturedData!, "Message").Should().Be(message); TestHelpers.GetPropertyValue(capturedData!, "Type").Should().Be(type); TestHelpers.GetPropertyValue(capturedData!, "Timestamp").Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5)); } [Fact] public async Task MultipleNotifications_AllSentCorrectly() { // Arrange var tenantId = Guid.NewGuid(); var projectId = Guid.NewGuid(); var userId = Guid.NewGuid(); // Act await _service.NotifyProjectCreated(tenantId, projectId, new { Name = "Project1" }); await _service.NotifyUser(userId, "Hello"); await _service.NotifyUsersInTenant(tenantId, "Announcement"); // Assert _mockClientProxy.Verify(p => p.SendCoreAsync( It.IsAny(), It.IsAny(), default), Times.Exactly(3)); } }