Files
ColaFlow/colaflow-api/tests/Modules/Mcp/ColaFlow.Modules.Mcp.Tests/Services/McpResourceRegistryTests.cs
Yaojia Wang 3ab505e0f6 feat(backend): Implement Story 5.6 - Resource Registration & Discovery
Implemented pluggable resource registration and auto-discovery mechanism for MCP Resources.

Changes:
- Enhanced McpResourceDescriptor with metadata (Category, Version, Parameters, Examples, Tags, IsEnabled)
- Created ResourceDiscoveryService for Assembly scanning and auto-discovery
- Updated McpResourceRegistry with category support and grouping methods
- Enhanced ResourcesListMethodHandler to return categorized resources with full metadata
- Created ResourceHealthCheckHandler for resource availability verification
- Updated all existing Resources (Projects, Issues, Sprints, Users) with Categories and Versions
- Updated McpServiceExtensions to use auto-discovery at startup
- Added comprehensive unit tests for discovery and health check

Features:
 New Resources automatically discovered via Assembly scanning
 Resources organized by category (Projects, Issues, Sprints, Users)
 Rich metadata for documentation (parameters, examples, tags)
 Health check endpoint (resources/health) for monitoring
 Thread-safe registry operations

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 16:07:50 +01:00

295 lines
10 KiB
C#

using ColaFlow.Modules.Mcp.Application.Services;
using ColaFlow.Modules.Mcp.Contracts.Resources;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using NSubstitute;
namespace ColaFlow.Modules.Mcp.Tests.Services;
/// <summary>
/// Unit tests for McpResourceRegistry
/// </summary>
public class McpResourceRegistryTests
{
private readonly ILogger<McpResourceRegistry> _logger;
private readonly McpResourceRegistry _sut;
public McpResourceRegistryTests()
{
_logger = Substitute.For<ILogger<McpResourceRegistry>>();
_sut = new McpResourceRegistry(_logger);
}
[Fact]
public void RegisterResource_AddsResourceToRegistry()
{
// Arrange
var resource = CreateMockResource("colaflow://test", "Test Resource");
// Act
_sut.RegisterResource(resource);
// Assert
var resources = _sut.GetAllResources();
resources.Should().ContainSingle();
resources[0].Uri.Should().Be("colaflow://test");
}
[Fact]
public void RegisterResource_WithDuplicateUri_Overwrites()
{
// Arrange
var resource1 = CreateMockResource("colaflow://test", "Test Resource 1");
var resource2 = CreateMockResource("colaflow://test", "Test Resource 2");
// Act
_sut.RegisterResource(resource1);
_sut.RegisterResource(resource2);
// Assert
var resources = _sut.GetAllResources();
resources.Count.Should().Be(2); // Both are in the list
var foundResource = _sut.GetResourceByUri("colaflow://test");
foundResource!.Name.Should().Be("Test Resource 2"); // But only the last one is returned
}
[Fact]
public void GetResourceByUri_WithExactMatch_ReturnsResource()
{
// Arrange
var resource = CreateMockResource("colaflow://projects.list", "Projects List");
_sut.RegisterResource(resource);
// Act
var result = _sut.GetResourceByUri("colaflow://projects.list");
// Assert
result.Should().NotBeNull();
result!.Uri.Should().Be("colaflow://projects.list");
}
[Fact]
public void GetResourceByUri_WithTemplateMatch_ReturnsResource()
{
// Arrange
var resource = CreateMockResource("colaflow://projects.get/{id}", "Project Details");
_sut.RegisterResource(resource);
// Act
var result = _sut.GetResourceByUri("colaflow://projects.get/123");
// Assert
result.Should().NotBeNull();
result!.Uri.Should().Be("colaflow://projects.get/{id}");
}
[Fact]
public void GetResourceByUri_WithNonExistentUri_ReturnsNull()
{
// Act
var result = _sut.GetResourceByUri("colaflow://nonexistent");
// Assert
result.Should().BeNull();
}
[Fact]
public void GetResourceDescriptors_ReturnsAllDescriptors()
{
// Arrange
var resource1 = CreateMockResource("colaflow://test1", "Test 1");
var resource2 = CreateMockResource("colaflow://test2", "Test 2");
_sut.RegisterResource(resource1);
_sut.RegisterResource(resource2);
// Act
var descriptors = _sut.GetResourceDescriptors();
// Assert
descriptors.Should().HaveCount(2);
descriptors.Should().Contain(d => d.Uri == "colaflow://test1");
descriptors.Should().Contain(d => d.Uri == "colaflow://test2");
}
[Fact]
public void GetAllResources_ReturnsReadOnlyList()
{
// Arrange
var resource = CreateMockResource("colaflow://test", "Test");
_sut.RegisterResource(resource);
// Act
var resources = _sut.GetAllResources();
// Assert
resources.Should().BeAssignableTo<IReadOnlyList<IMcpResource>>();
}
private IMcpResource CreateMockResource(string uri, string name)
{
var resource = Substitute.For<IMcpResource>();
resource.UriReturns(uri);
resource.NameReturns(name);
resource.DescriptionReturns($"Description for {name}");
resource.MimeTypeReturns("application/json");
resource.GetContentAsync(Arg.Any<McpResourceRequest>(), Arg.Any<CancellationToken>())
Returns(new McpResourceContent
{
Uri = uri,
MimeType = "application/json",
Text = "{}"
});
return resource;
}
[Fact]
public void GetCategories_ShouldReturnAllUniqueCategories()
{
// Arrange
var mockLogger = Substitute.For<ILogger<McpResourceRegistry>>();
var registry = new McpResourceRegistry(mockLogger);
var resource1 = Substitute.For<IMcpResource>();
resource1.Setup(r => r.Uri)Returns("test://resource1");
resource1.Setup(r => r.Name)Returns("Resource 1");
resource1.Setup(r => r.Category)Returns("Projects");
var resource2 = Substitute.For<IMcpResource>();
resource2.Setup(r => r.Uri)Returns("test://resource2");
resource2.Setup(r => r.Name)Returns("Resource 2");
resource2.Setup(r => r.Category)Returns("Issues");
var resource3 = Substitute.For<IMcpResource>();
resource3.Setup(r => r.Uri)Returns("test://resource3");
resource3.Setup(r => r.Name)Returns("Resource 3");
resource3.Setup(r => r.Category)Returns("Projects"); // Duplicate category
registry.RegisterResource(resource1.Object);
registry.RegisterResource(resource2.Object);
registry.RegisterResource(resource3.Object);
// Act
var categories = registry.GetCategories();
// Assert
Assert.Equal(2, categories.Count);
Assert.Contains("Projects", categories);
Assert.Contains("Issues", categories);
}
[Fact]
public void GetResourcesByCategory_ShouldReturnOnlyResourcesInCategory()
{
// Arrange
var mockLogger = Substitute.For<ILogger<McpResourceRegistry>>();
var registry = new McpResourceRegistry(mockLogger);
var projectResource1 = Substitute.For<IMcpResource>();
projectResource1.Setup(r => r.Uri)Returns("test://project1");
projectResource1.Setup(r => r.Name)Returns("Project 1");
projectResource1.Setup(r => r.Category)Returns("Projects");
var projectResource2 = Substitute.For<IMcpResource>();
projectResource2.Setup(r => r.Uri)Returns("test://project2");
projectResource2.Setup(r => r.Name)Returns("Project 2");
projectResource2.Setup(r => r.Category)Returns("Projects");
var issueResource = Substitute.For<IMcpResource>();
issueResource.Setup(r => r.Uri)Returns("test://issue1");
issueResource.Setup(r => r.Name)Returns("Issue 1");
issueResource.Setup(r => r.Category)Returns("Issues");
registry.RegisterResource(projectResource1.Object);
registry.RegisterResource(projectResource2.Object);
registry.RegisterResource(issueResource.Object);
// Act
var projectResources = registry.GetResourcesByCategory("Projects");
// Assert
Assert.Equal(2, projectResources.Count);
Assert.All(projectResources, r => Assert.Equal("Projects", r.Category));
}
[Fact]
public void GetResourcesGroupedByCategory_ShouldGroupCorrectly()
{
// Arrange
var mockLogger = Substitute.For<ILogger<McpResourceRegistry>>();
var registry = new McpResourceRegistry(mockLogger);
var resource1 = Substitute.For<IMcpResource>();
resource1.Setup(r => r.Uri)Returns("test://resource1");
resource1.Setup(r => r.Name)Returns("Resource 1");
resource1.Setup(r => r.Category)Returns("Projects");
var resource2 = Substitute.For<IMcpResource>();
resource2.Setup(r => r.Uri)Returns("test://resource2");
resource2.Setup(r => r.Name)Returns("Resource 2");
resource2.Setup(r => r.Category)Returns("Issues");
var resource3 = Substitute.For<IMcpResource>();
resource3.Setup(r => r.Uri)Returns("test://resource3");
resource3.Setup(r => r.Name)Returns("Resource 3");
resource3.Setup(r => r.Category)Returns("Projects");
registry.RegisterResource(resource1.Object);
registry.RegisterResource(resource2.Object);
registry.RegisterResource(resource3.Object);
// Act
var grouped = registry.GetResourcesGroupedByCategory();
// Assert
Assert.Equal(2, grouped.Count);
Assert.True(grouped.ContainsKey("Projects"));
Assert.True(grouped.ContainsKey("Issues"));
Assert.Equal(2, grouped["Projects"].Count);
Assert.Single(grouped["Issues"]);
}
[Fact]
public void GetDescriptor_ShouldIncludeEnhancedMetadata()
{
// Arrange
var mockLogger = Substitute.For<ILogger<McpResourceRegistry>>();
var registry = new McpResourceRegistry(mockLogger);
var mockResource = Substitute.For<IMcpResource>();
mockResource(r => r.Uri)Returns("test://resource");
mockResource(r => r.Name)Returns("Test Resource");
mockResource(r => r.Description)Returns("Test description");
mockResource(r => r.MimeType)Returns("application/json");
mockResource(r => r.Category)Returns("Testing");
mockResource(r => r.Version)Returns("1.0");
mockResource(r => r.GetDescriptor())Returns(new McpResourceDescriptor
{
Uri = "test://resource",
Name = "Test Resource",
Description = "Test description",
MimeType = "application/json",
Category = "Testing",
Version = "1.0",
Parameters = new Dictionary<string, string> { { "id", "Resource ID" } },
Examples = new List<string> { "GET test://resource?id=123" },
Tags = new List<string> { "test", "example" },
IsEnabled = true
});
registry.RegisterResource(mockResource);
// Act
var descriptors = registry.GetResourceDescriptors();
// Assert
Assert.Single(descriptors);
var descriptor = descriptors[0];
Assert.Equal("Testing", descriptor.Category);
Assert.Equal("1.0", descriptor.Version);
Assert.NotNull(descriptor.Parameters);
Assert.NotNull(descriptor.Examples);
Assert.NotNull(descriptor.Tags);
Assert.True(descriptor.IsEnabled);
}
}