File: ApplicationModel\McpServerEndpointAnnotationTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Net.Sockets;
 
namespace Aspire.Hosting.Tests.ApplicationModel;
 
public class McpServerEndpointAnnotationTests
{
    [Fact]
    public void Constructor_ThrowsArgumentNullException_WhenResolverIsNull()
    {
        Assert.Throws<ArgumentNullException>(() => new McpServerEndpointAnnotation(null!));
    }
 
    [Fact]
    public async Task Constructor_WithCustomResolver_UsesProvidedResolver()
    {
        var expectedUri = new Uri("http://custom.example.com:9000/mcp");
        var annotation = new McpServerEndpointAnnotation((resource, ct) => Task.FromResult<Uri?>(expectedUri));
 
        var resource = new TestResourceWithEndpoints("test");
        var result = await annotation.EndpointUrlResolver(resource, CancellationToken.None);
 
        Assert.Equal(expectedUri, result);
    }
 
    [Fact]
    public async Task Constructor_WithCustomResolver_CanReturnNull()
    {
        var annotation = new McpServerEndpointAnnotation((resource, ct) => Task.FromResult<Uri?>(null));
 
        var resource = new TestResourceWithEndpoints("test");
        var result = await annotation.EndpointUrlResolver(resource, CancellationToken.None);
 
        Assert.Null(result);
    }
 
    [Fact]
    public void FromEndpoint_ThrowsArgumentNullException_WhenEndpointNameIsNull()
    {
        Assert.Throws<ArgumentNullException>(() => McpServerEndpointAnnotation.FromEndpoint(null!));
    }
 
    [Fact]
    public void FromEndpoint_ThrowsArgumentException_WhenEndpointNameIsEmpty()
    {
        Assert.Throws<ArgumentException>(() => McpServerEndpointAnnotation.FromEndpoint(""));
    }
 
    [Fact]
    public void FromEndpoint_ThrowsArgumentException_WhenEndpointNameIsWhitespace()
    {
        Assert.Throws<ArgumentException>(() => McpServerEndpointAnnotation.FromEndpoint("   "));
    }
 
    [Fact]
    public async Task FromEndpoint_ReturnsNull_WhenEndpointDoesNotExist()
    {
        var annotation = McpServerEndpointAnnotation.FromEndpoint("nonexistent");
 
        var resource = new TestResourceWithEndpoints("test");
        var result = await annotation.EndpointUrlResolver(resource, CancellationToken.None);
 
        Assert.Null(result);
    }
 
    [Fact]
    public async Task FromEndpoint_ReturnsUrlWithDefaultPath_WhenEndpointExists()
    {
        var resource = new TestResourceWithEndpoints("test");
        var endpointAnnotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(endpointAnnotation);
        endpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint(endpointAnnotation, "localhost", 8080);
 
        var annotation = McpServerEndpointAnnotation.FromEndpoint("http");
        var result = await annotation.EndpointUrlResolver(resource, CancellationToken.None);
 
        Assert.NotNull(result);
        Assert.Equal("http://localhost:8080/mcp", result.ToString());
    }
 
    [Fact]
    public async Task FromEndpoint_ReturnsUrlWithCustomPath_WhenPathSpecified()
    {
        var resource = new TestResourceWithEndpoints("test");
        var endpointAnnotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(endpointAnnotation);
        endpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint(endpointAnnotation, "localhost", 8080);
 
        var annotation = McpServerEndpointAnnotation.FromEndpoint("http", "/sse");
        var result = await annotation.EndpointUrlResolver(resource, CancellationToken.None);
 
        Assert.NotNull(result);
        Assert.Equal("http://localhost:8080/sse", result.ToString());
    }
 
    [Fact]
    public async Task FromEndpoint_ReturnsBaseUrl_WhenPathIsNull()
    {
        var resource = new TestResourceWithEndpoints("test");
        var endpointAnnotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(endpointAnnotation);
        endpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint(endpointAnnotation, "localhost", 8080);
 
        var annotation = McpServerEndpointAnnotation.FromEndpoint("http", null);
        var result = await annotation.EndpointUrlResolver(resource, CancellationToken.None);
 
        Assert.NotNull(result);
        // Uri normalizes to include trailing slash for absolute URIs without path
        Assert.Equal("http://localhost:8080/", result.ToString());
    }
 
    [Fact]
    public async Task FromEndpoint_ReturnsBaseUrl_WhenPathIsEmpty()
    {
        var resource = new TestResourceWithEndpoints("test");
        var endpointAnnotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(endpointAnnotation);
        endpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint(endpointAnnotation, "localhost", 8080);
 
        var annotation = McpServerEndpointAnnotation.FromEndpoint("http", "");
        var result = await annotation.EndpointUrlResolver(resource, CancellationToken.None);
 
        Assert.NotNull(result);
        // Uri normalizes to include trailing slash for absolute URIs without path
        Assert.Equal("http://localhost:8080/", result.ToString());
    }
 
    [Fact]
    public async Task FromEndpoint_NormalizesPathWithoutLeadingSlash()
    {
        var resource = new TestResourceWithEndpoints("test");
        var endpointAnnotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(endpointAnnotation);
        endpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint(endpointAnnotation, "localhost", 8080);
 
        var annotation = McpServerEndpointAnnotation.FromEndpoint("http", "custom/path");
        var result = await annotation.EndpointUrlResolver(resource, CancellationToken.None);
 
        Assert.NotNull(result);
        Assert.Equal("http://localhost:8080/custom/path", result.ToString());
    }
 
    [Fact]
    public async Task FromEndpoint_HandlesTrailingSlashInBaseUrl()
    {
        var resource = new TestResourceWithEndpoints("test");
        var endpointAnnotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(endpointAnnotation);
        endpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint(endpointAnnotation, "localhost", 8080);
 
        var annotation = McpServerEndpointAnnotation.FromEndpoint("http", "/mcp");
        var result = await annotation.EndpointUrlResolver(resource, CancellationToken.None);
 
        Assert.NotNull(result);
        Assert.Equal("http://localhost:8080/mcp", result.ToString());
    }
 
    [Fact]
    public async Task FromEndpoint_WaitsForEndpointAllocation()
    {
        var resource = new TestResourceWithEndpoints("test");
        var endpointAnnotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http");
        resource.Annotations.Add(endpointAnnotation);
        // Not setting AllocatedEndpoint initially
 
        var annotation = McpServerEndpointAnnotation.FromEndpoint("http");
 
        using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
 
        // The resolver will wait for allocation, so we expect it to be cancelled
        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            () => annotation.EndpointUrlResolver(resource, cts.Token));
    }
 
    [Fact]
    public async Task FromEndpoint_WorksWithHttpsEndpoint()
    {
        var resource = new TestResourceWithEndpoints("test");
        var endpointAnnotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "https", name: "https");
        resource.Annotations.Add(endpointAnnotation);
        endpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint(endpointAnnotation, "localhost", 8443);
 
        var annotation = McpServerEndpointAnnotation.FromEndpoint("https", "/mcp");
        var result = await annotation.EndpointUrlResolver(resource, CancellationToken.None);
 
        Assert.NotNull(result);
        Assert.Equal("https://localhost:8443/mcp", result.ToString());
    }
 
    private sealed class TestResourceWithEndpoints(string name) : Resource(name), IResourceWithEndpoints
    {
    }
}