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>
234 lines
7.8 KiB
C#
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);
|
|
}
|
|
}
|