using ColaFlow.Modules.Mcp.Application.DTOs.Notifications; using ColaFlow.Modules.Mcp.Infrastructure.Hubs; using ColaFlow.Modules.Mcp.Infrastructure.Services; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; using Moq; using Xunit; namespace ColaFlow.Modules.Mcp.Tests.Services; public class McpNotificationServiceTests { private readonly Mock> _mockHubContext; private readonly Mock> _mockLogger; private readonly Mock _mockClients; private readonly Mock _mockClientProxy; private readonly McpNotificationService _service; public McpNotificationServiceTests() { _mockHubContext = new Mock>(); _mockLogger = new Mock>(); _mockClients = new Mock(); _mockClientProxy = new Mock(); _mockHubContext.Setup(h => h.Clients).Returns(_mockClients.Object); _service = new McpNotificationService(_mockHubContext.Object, _mockLogger.Object); } [Fact] public async Task NotifyPendingChangeCreatedAsync_SendsNotificationToCorrectGroups() { // Arrange var pendingChangeId = Guid.NewGuid(); var tenantId = Guid.NewGuid(); var notification = new PendingChangeCreatedNotification { NotificationType = "PendingChangeCreated", PendingChangeId = pendingChangeId, ToolName = "create_epic", EntityType = "Epic", Operation = "CREATE", Summary = "Create Epic: Test Epic", TenantId = tenantId }; _mockClients.Setup(c => c.Groups(It.IsAny(), It.IsAny())) .Returns(_mockClientProxy.Object); // Act await _service.NotifyPendingChangeCreatedAsync(notification); // Assert _mockClients.Verify( c => c.Groups($"pending-change-{pendingChangeId}", $"tenant-{tenantId}"), Times.Once); _mockClientProxy.Verify( p => p.SendCoreAsync( "PendingChangeCreated", It.Is(args => args.Length == 1 && args[0] == notification), It.IsAny()), Times.Once); } [Fact] public async Task NotifyPendingChangeApprovedAsync_SendsNotificationWithCorrectData() { // Arrange var pendingChangeId = Guid.NewGuid(); var tenantId = Guid.NewGuid(); var approvedBy = Guid.NewGuid(); var entityId = Guid.NewGuid(); var notification = new PendingChangeApprovedNotification { NotificationType = "PendingChangeApproved", PendingChangeId = pendingChangeId, ToolName = "create_epic", EntityType = "Epic", Operation = "CREATE", EntityId = entityId, ApprovedBy = approvedBy, ExecutionResult = "Epic created: Test Epic", TenantId = tenantId }; _mockClients.Setup(c => c.Groups(It.IsAny(), It.IsAny())) .Returns(_mockClientProxy.Object); // Act await _service.NotifyPendingChangeApprovedAsync(notification); // Assert _mockClientProxy.Verify( p => p.SendCoreAsync( "PendingChangeApproved", It.Is(args => args.Length == 1 && ((PendingChangeApprovedNotification)args[0]).ApprovedBy == approvedBy && ((PendingChangeApprovedNotification)args[0]).EntityId == entityId), It.IsAny()), Times.Once); } [Fact] public async Task NotifyPendingChangeRejectedAsync_SendsNotificationWithReason() { // Arrange var pendingChangeId = Guid.NewGuid(); var tenantId = Guid.NewGuid(); var rejectedBy = Guid.NewGuid(); var reason = "Epic name is too vague"; var notification = new PendingChangeRejectedNotification { NotificationType = "PendingChangeRejected", PendingChangeId = pendingChangeId, ToolName = "create_epic", Reason = reason, RejectedBy = rejectedBy, TenantId = tenantId }; _mockClients.Setup(c => c.Groups(It.IsAny(), It.IsAny())) .Returns(_mockClientProxy.Object); // Act await _service.NotifyPendingChangeRejectedAsync(notification); // Assert _mockClientProxy.Verify( p => p.SendCoreAsync( "PendingChangeRejected", It.Is(args => args.Length == 1 && ((PendingChangeRejectedNotification)args[0]).Reason == reason && ((PendingChangeRejectedNotification)args[0]).RejectedBy == rejectedBy), It.IsAny()), Times.Once); } [Fact] public async Task NotifyPendingChangeAppliedAsync_SendsNotificationWithResult() { // Arrange var pendingChangeId = Guid.NewGuid(); var tenantId = Guid.NewGuid(); var result = "Epic created: abc123 - Test Epic"; var notification = new PendingChangeAppliedNotification { NotificationType = "PendingChangeApplied", PendingChangeId = pendingChangeId, ToolName = "create_epic", Result = result, AppliedAt = DateTime.UtcNow, TenantId = tenantId }; _mockClients.Setup(c => c.Groups(It.IsAny(), It.IsAny())) .Returns(_mockClientProxy.Object); // Act await _service.NotifyPendingChangeAppliedAsync(notification); // Assert _mockClientProxy.Verify( p => p.SendCoreAsync( "PendingChangeApplied", It.Is(args => args.Length == 1 && ((PendingChangeAppliedNotification)args[0]).Result == result), It.IsAny()), Times.Once); } [Fact] public async Task NotifyPendingChangeExpiredAsync_SendsNotificationToCorrectGroups() { // Arrange var pendingChangeId = Guid.NewGuid(); var tenantId = Guid.NewGuid(); var notification = new PendingChangeExpiredNotification { NotificationType = "PendingChangeExpired", PendingChangeId = pendingChangeId, ToolName = "create_epic", ExpiredAt = DateTime.UtcNow, TenantId = tenantId }; _mockClients.Setup(c => c.Groups(It.IsAny(), It.IsAny())) .Returns(_mockClientProxy.Object); // Act await _service.NotifyPendingChangeExpiredAsync(notification); // Assert _mockClients.Verify( c => c.Groups($"pending-change-{pendingChangeId}", $"tenant-{tenantId}"), Times.Once); _mockClientProxy.Verify( p => p.SendCoreAsync( "PendingChangeExpired", It.Is(args => args.Length == 1), It.IsAny()), Times.Once); } [Fact] public void Constructor_ThrowsArgumentNullException_WhenHubContextIsNull() { // Act & Assert var exception = Assert.Throws(() => new McpNotificationService(null!, _mockLogger.Object)); Assert.Equal("hubContext", exception.ParamName); } [Fact] public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull() { // Act & Assert var exception = Assert.Throws(() => new McpNotificationService(_mockHubContext.Object, null!)); Assert.Equal("logger", exception.ParamName); } }