File: Mcp\ApiDocs\ApiMemberMarkdownParserTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Cli.Documentation.ApiDocs;
 
namespace Aspire.Cli.Tests.Documentation.ApiDocs;
 
public class ApiMemberMarkdownParserTests
{
    [Fact]
    public void Parse_UsesKnownMemberGroupRoutes()
    {
        var containerItem = new ApiReferenceItem
        {
            Id = "csharp/aspire.test.package/testresource",
            Name = "TestResource",
            Language = ApiReferenceLanguages.CSharp,
            Kind = ApiReferenceKinds.Type,
            PageUrl = "https://aspire.dev/reference/api/csharp/aspire.test.package/testresource"
        };
 
        var memberGroupItem = new ApiReferenceItem
        {
            Id = "csharp/aspire.test.package/testresource/methods",
            Name = "methods",
            Language = ApiReferenceLanguages.CSharp,
            Kind = ApiReferenceKinds.MemberGroup,
            PageUrl = "https://aspire.dev/reference/api/csharp/aspire.test.package/testresource/methods",
            ParentId = containerItem.Id,
            MemberGroup = "methods"
        };
 
        var items = ApiMemberMarkdownParser.Parse(
            containerItem,
            """
            # TestResource
 
            ## Methods
 
            - [RunEmulator()](/reference/api/csharp/aspire.test.package/testresource/methods.md#runemulator) : `void` -- Runs the emulator.
            - [OtherType](/reference/api/csharp/aspire.test.package/othertype.md) -- Should not be treated as a member.
            """,
            ApiDocsSourceConfiguration.DefaultSitemapUrl,
            new Dictionary<string, ApiReferenceItem>(StringComparer.OrdinalIgnoreCase)
            {
                [memberGroupItem.PageUrl] = memberGroupItem
            });
 
        var item = Assert.Single(items);
        Assert.Equal("csharp/aspire.test.package/testresource/methods#runemulator", item.Id);
        Assert.Equal("RunEmulator()", item.Name);
        Assert.Equal(ApiReferenceLanguages.CSharp, item.Language);
        Assert.Equal(ApiReferenceKinds.Member, item.Kind);
        Assert.Equal(memberGroupItem.Id, item.ParentId);
        Assert.Equal("Runs the emulator.", item.Summary);
    }
 
    [Fact]
    public void Parse_ShortenOverloadIdsWhileKeepingThemDistinct()
    {
        var containerItem = new ApiReferenceItem
        {
            Id = "csharp/aspire.test.package/resourcebuilderextensions",
            Name = "ResourceBuilderExtensions",
            Language = ApiReferenceLanguages.CSharp,
            Kind = ApiReferenceKinds.Type,
            PageUrl = "https://aspire.dev/reference/api/csharp/aspire.test.package/resourcebuilderextensions"
        };
 
        var memberGroupItem = new ApiReferenceItem
        {
            Id = "csharp/aspire.test.package/resourcebuilderextensions/methods",
            Name = "methods",
            Language = ApiReferenceLanguages.CSharp,
            Kind = ApiReferenceKinds.MemberGroup,
            PageUrl = "https://aspire.dev/reference/api/csharp/aspire.test.package/resourcebuilderextensions/methods",
            ParentId = containerItem.Id,
            MemberGroup = "methods"
        };
 
        var items = ApiMemberMarkdownParser.Parse(
            containerItem,
            """
            # ResourceBuilderExtensions
 
            ## Methods
 
            - [WithEnvironment(IResourceBuilder<T>, string, string?)](/reference/api/csharp/aspire.test.package/resourcebuilderextensions/methods.md#withenvironment-iresourcebuilder-t-string-string) : `IResourceBuilder<T>` -- Adds a string environment variable.
            - [WithEnvironment(IResourceBuilder<T>, Action<EnvironmentCallbackContext>)](/reference/api/csharp/aspire.test.package/resourcebuilderextensions/methods.md#withenvironment-iresourcebuilder-t-action-environmentcallbackcontext) : `IResourceBuilder<T>` -- Adds environment variables from a callback.
            """,
            ApiDocsSourceConfiguration.DefaultSitemapUrl,
            new Dictionary<string, ApiReferenceItem>(StringComparer.OrdinalIgnoreCase)
            {
                [memberGroupItem.PageUrl] = memberGroupItem
            });
 
        Assert.Equal(2, items.Count);
        Assert.All(items, item =>
        {
            Assert.True(item.Id.StartsWith("csharp/aspire.test.package/resourcebuilderextensions/methods#withenvironment-", StringComparison.Ordinal), item.Id);
            Assert.DoesNotContain("iresourcebuilder", item.Id, StringComparison.OrdinalIgnoreCase);
        });
        Assert.NotEqual(items[0].Id, items[1].Id);
    }
 
    [Fact]
    public void Parse_IgnoresUnknownSectionsAndFencedCodeAroundMemberLinks()
    {
        // Only C# pages carry grouped member bullets, so the Go-shaped sections are mixed into a
        // C# page to prove that unrecognized sections and fenced code don't disturb extraction.
        var containerItem = new ApiReferenceItem
        {
            Id = "csharp/aspire.test.package/testresource",
            Name = "TestResource",
            Language = ApiReferenceLanguages.CSharp,
            Kind = ApiReferenceKinds.Type,
            PageUrl = "https://aspire.dev/reference/api/csharp/aspire.test.package/testresource"
        };
 
        var items = ApiMemberMarkdownParser.Parse(
            containerItem,
            """
            # TestResource
 
            ## Definition
 
            ```go
            func (r *testResource) RunEmulator() error
            ```
 
            ## Methods
 
            - [RunEmulator()](/reference/api/csharp/aspire.test.package/testresource/methods.md#runemulator) : `void` -- Runs the emulator.
 
            ## Options
 
            - `Port` (`*float64`) -- The host port to bind.
 
            ## Errors
 
            Builder methods defer failures. Call `Err()` on the returned handle to observe them.
            """,
            ApiDocsSourceConfiguration.DefaultSitemapUrl,
            CreateCSharpMemberGroups());
 
        var item = Assert.Single(items);
        Assert.Equal("csharp/aspire.test.package/testresource/methods#runemulator", item.Id);
        Assert.Equal("RunEmulator()", item.Name);
        Assert.Equal("Runs the emulator.", item.Summary);
    }
 
    // Go method lists use the same "- [Name](link)" shape as C# member overview lines. Only the known
    // grouped member pages decide whether a link becomes a member, and Go has no grouped member pages.
    [Fact]
    public void Parse_ReturnsNoMembersForGoItemPageWhenNoGoMemberGroupsAreKnown()
    {
        var containerItem = new ApiReferenceItem
        {
            Id = "go/aspire.hosting/containerresource",
            Name = "containerresource",
            Language = ApiReferenceLanguages.Go,
            Kind = ApiReferenceKinds.Symbol,
            PageUrl = "https://aspire.dev/reference/api/go/aspire.hosting/containerresource",
            ParentId = "go/aspire.hosting"
        };
 
        var items = ApiMemberMarkdownParser.Parse(
            containerItem,
            """
            # ContainerResource
 
            - Module: `aspire.hosting`
            - Kind: `interface`
 
            Represents a container resource.
 
            ## Definition
 
            ```go
            type ContainerResource interface {
                handleReference
                WithEndpoint(options ...*WithEndpointOptions) ContainerResource
                WithEnvironment(name string, value any) ContainerResource
                Err() error
            }
            ```
 
            ## Methods
 
            - [WithEndpoint](/reference/api/go/aspire.hosting/containerresource/withendpoint.md) -- `method` -- Adds a network endpoint.
            - [WithEnvironment](/reference/api/go/aspire.hosting/containerresource/withenvironment.md) -- `method` -- Sets an environment variable.
 
            ## Errors
 
            Builder methods defer failures. Call `Err()` on the returned handle to observe them.
 
            ## Applies to
 
            - [ContainerResource](/reference/api/go/aspire.hosting/containerresource.md)
            """,
            ApiDocsSourceConfiguration.DefaultSitemapUrl,
            CreateCSharpMemberGroups());
 
        Assert.Empty(items);
    }
 
    [Fact]
    public void Parse_ReturnsNoMembersForGoMemberPageWhenNoGoMemberGroupsAreKnown()
    {
        var containerItem = new ApiReferenceItem
        {
            Id = "go/aspire.hosting/containerresource/withendpoint",
            Name = "withendpoint",
            Language = ApiReferenceLanguages.Go,
            Kind = ApiReferenceKinds.Member,
            PageUrl = "https://aspire.dev/reference/api/go/aspire.hosting/containerresource/withendpoint",
            ParentId = "go/aspire.hosting/containerresource"
        };
 
        var items = ApiMemberMarkdownParser.Parse(
            containerItem,
            """
            # WithEndpoint
 
            Adds a network endpoint to the container resource.
 
            ## Definition
 
            ```go
            func (r *containerResource) WithEndpoint(options ...*WithEndpointOptions) ContainerResource
            ```
 
            ## Signature
 
            ```go
            WithEndpoint(options ...*WithEndpointOptions) ContainerResource
            ```
 
            ## Parameters
 
            - `options` (`...*WithEndpointOptions`) -- Optional parameters. See Options.
 
            ## Options
 
            - `Port` (`*float64`) -- The host port to bind.
            - `Name` (`*string`) -- The endpoint name.
 
            ## Returns
 
            - [ContainerResource](/reference/api/go/aspire.hosting/containerresource.md) -- The receiver handle. Failures are deferred, so check `Err()` for the `error`.
 
            ## Defined on
 
            - [ContainerResource](/reference/api/go/aspire.hosting/containerresource.md)
            """,
            ApiDocsSourceConfiguration.DefaultSitemapUrl,
            CreateCSharpMemberGroups());
 
        Assert.Empty(items);
    }
 
    // The grouped member pages are C# only. A non-empty lookup keeps Parse from returning early,
    // so the markdown is actually scanned.
    private static Dictionary<string, ApiReferenceItem> CreateCSharpMemberGroups()
    {
        var memberGroupItem = new ApiReferenceItem
        {
            Id = "csharp/aspire.test.package/testresource/methods",
            Name = "methods",
            Language = ApiReferenceLanguages.CSharp,
            Kind = ApiReferenceKinds.MemberGroup,
            PageUrl = "https://aspire.dev/reference/api/csharp/aspire.test.package/testresource/methods",
            ParentId = "csharp/aspire.test.package/testresource",
            MemberGroup = "methods"
        };
 
        return new Dictionary<string, ApiReferenceItem>(StringComparer.OrdinalIgnoreCase)
        {
            [memberGroupItem.PageUrl] = memberGroupItem
        };
    }
}