Files
ColaFlow/colaflow-api/tests/Modules/Mcp/ColaFlow.Modules.Mcp.Tests/Services/McpNotificationServiceTests.cs
Yaojia Wang 1d6e732018
Some checks failed
Code Coverage / Generate Coverage Report (push) Has been cancelled
Tests / Run Tests (9.0.x) (push) Has been cancelled
Tests / Docker Build Test (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
fix(backend): Move McpNotificationHub to Infrastructure layer to fix dependency inversion violation
Fixed compilation error where Infrastructure layer was referencing API layer (ColaFlow.API.Hubs).
This violated the dependency inversion principle and Clean Architecture layering rules.

Changes:
- Moved McpNotificationHub from ColaFlow.API/Hubs to ColaFlow.Modules.Mcp.Infrastructure/Hubs
- Updated McpNotificationHub to inherit directly from Hub instead of BaseHub
- Copied necessary helper methods (GetCurrentUserId, GetCurrentTenantId, GetTenantGroupName) to avoid cross-layer dependency
- Updated McpNotificationService to use new namespace (ColaFlow.Modules.Mcp.Infrastructure.Hubs)
- Updated Program.cs to import new Hub namespace
- Updated McpNotificationServiceTests to use new namespace
- Kept BaseHub in API layer for ProjectHub and NotificationHub

Architecture Impact:
- Infrastructure layer no longer depends on API layer
- Proper dependency flow: API -> Infrastructure -> Application -> Domain
- McpNotificationHub is now properly encapsulated within the MCP module

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 18:37:08 +01:00

234 lines
7.8 KiB
C#

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<IHubContext<McpNotificationHub>> _mockHubContext;
private readonly Mock<ILogger<McpNotificationService>> _mockLogger;
private readonly Mock<IHubClients> _mockClients;
private readonly Mock<IClientProxy> _mockClientProxy;
private readonly McpNotificationService _service;
public McpNotificationServiceTests()
{
_mockHubContext = new Mock<IHubContext<McpNotificationHub>>();
_mockLogger = new Mock<ILogger<McpNotificationService>>();
_mockClients = new Mock<IHubClients>();
_mockClientProxy = new Mock<IClientProxy>();
_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<string>(), It.IsAny<string>()))
.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<object[]>(args => args.Length == 1 && args[0] == notification),
It.IsAny<CancellationToken>()),
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<string>(), It.IsAny<string>()))
.Returns(_mockClientProxy.Object);
// Act
await _service.NotifyPendingChangeApprovedAsync(notification);
// Assert
_mockClientProxy.Verify(
p => p.SendCoreAsync(
"PendingChangeApproved",
It.Is<object[]>(args =>
args.Length == 1 &&
((PendingChangeApprovedNotification)args[0]).ApprovedBy == approvedBy &&
((PendingChangeApprovedNotification)args[0]).EntityId == entityId),
It.IsAny<CancellationToken>()),
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<string>(), It.IsAny<string>()))
.Returns(_mockClientProxy.Object);
// Act
await _service.NotifyPendingChangeRejectedAsync(notification);
// Assert
_mockClientProxy.Verify(
p => p.SendCoreAsync(
"PendingChangeRejected",
It.Is<object[]>(args =>
args.Length == 1 &&
((PendingChangeRejectedNotification)args[0]).Reason == reason &&
((PendingChangeRejectedNotification)args[0]).RejectedBy == rejectedBy),
It.IsAny<CancellationToken>()),
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<string>(), It.IsAny<string>()))
.Returns(_mockClientProxy.Object);
// Act
await _service.NotifyPendingChangeAppliedAsync(notification);
// Assert
_mockClientProxy.Verify(
p => p.SendCoreAsync(
"PendingChangeApplied",
It.Is<object[]>(args =>
args.Length == 1 &&
((PendingChangeAppliedNotification)args[0]).Result == result),
It.IsAny<CancellationToken>()),
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<string>(), It.IsAny<string>()))
.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<object[]>(args => args.Length == 1),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public void Constructor_ThrowsArgumentNullException_WhenHubContextIsNull()
{
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
new McpNotificationService(null!, _mockLogger.Object));
Assert.Equal("hubContext", exception.ParamName);
}
[Fact]
public void Constructor_ThrowsArgumentNullException_WhenLoggerIsNull()
{
// Act & Assert
var exception = Assert.Throws<ArgumentNullException>(() =>
new McpNotificationService(_mockHubContext.Object, null!));
Assert.Equal("logger", exception.ParamName);
}
}