// 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.Docs;
using Aspire.Cli.Tests.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
namespace Aspire.Cli.Tests.Commands;
public class DocsCommandTests(ITestOutputHelper outputHelper)
{
[Fact]
public async Task DocsCommand_WithNoSubcommand_ShowsHelp()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.DocsIndexServiceFactory = _ => new TestDocsIndexService();
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
var result = command.Parse("docs");
var exitCode = await result.InvokeAsync().DefaultTimeout();
// Returns InvalidCommand exit code when no subcommand is provided (shows help)
Assert.Equal(CliExitCodes.InvalidCommand, exitCode);
}
[Fact]
public async Task DocsListCommand_ReturnsDocuments()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.DocsIndexServiceFactory = _ => new TestDocsIndexService();
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
var result = command.Parse("docs list");
var exitCode = await result.InvokeAsync().DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task DocsListCommand_WithJsonFormat_ReturnsJson()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.DocsIndexServiceFactory = _ => new TestDocsIndexService();
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
var result = command.Parse("docs list --format json");
var exitCode = await result.InvokeAsync().DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task DocsSearchCommand_WithQuery_ReturnsResults()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.DocsIndexServiceFactory = _ => new TestDocsIndexService();
options.DocsSearchServiceFactory = _ => new TestDocsSearchService();
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
var result = command.Parse("docs search redis");
var exitCode = await result.InvokeAsync().DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task DocsSearchCommand_WithLimit_RespectsLimit()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.DocsIndexServiceFactory = _ => new TestDocsIndexService();
options.DocsSearchServiceFactory = _ => new TestDocsSearchService();
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
var result = command.Parse("docs search redis -n 3");
var exitCode = await result.InvokeAsync().DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task DocsSearchCommand_WithJsonFormat_ReturnsJson()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.DocsIndexServiceFactory = _ => new TestDocsIndexService();
options.DocsSearchServiceFactory = _ => new TestDocsSearchService();
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
var result = command.Parse("docs search redis --format json");
var exitCode = await result.InvokeAsync().DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task DocsGetCommand_WithValidSlug_ReturnsContent()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.DocsIndexServiceFactory = _ => new TestDocsIndexService();
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
var result = command.Parse("docs get redis-integration");
var exitCode = await result.InvokeAsync().DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task DocsGetCommand_WithSection_ReturnsSection()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.DocsIndexServiceFactory = _ => new TestDocsIndexService();
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
var result = command.Parse("docs get redis-integration --section \"Getting Started\"");
var exitCode = await result.InvokeAsync().DefaultTimeout();
Assert.Equal(0, exitCode);
}
[Fact]
public async Task DocsGetCommand_WithRichMarkdown_PreservesReadableBlockOrder()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var outputWriter = new TestOutputTextWriter(outputHelper);
var document = new DocsContent
{
Title = "Docs Smoke Test",
Slug = "docs-smoke-test",
Summary = "Interactive rendering sample",
Content = """
# Docs Smoke Test
> Learn how to configure HTTPS endpoints with the [Aspire CLI](https://example.com/install) and `aspire run`.
## Steps
1. First item
Continued explanation.
* Nested item
## Commands
```bash
aspire docs get docs-smoke-test
```
## Settings
| Setting | Environment variable | Purpose |
| :------ | :------------------- | ------: |
| `Azure:SubscriptionId` | `Azure__SubscriptionId` | Target Azure subscription |
""",
Sections = ["Steps", "Commands", "Settings"]
};
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.OutputTextWriter = outputWriter;
options.DisableAnsi = true;
options.CliHostEnvironmentFactory = _ => TestHelpers.CreateNonInteractiveHostEnvironment();
options.DocsIndexServiceFactory = _ => new TestDocsIndexService(new Dictionary<string, DocsContent>
{
[document.Slug] = document
});
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
var result = command.Parse("docs get docs-smoke-test");
var exitCode = await result.InvokeAsync().DefaultTimeout();
Assert.Equal(0, exitCode);
var output = string.Join("\n", outputWriter.Logs);
Assert.Contains("Docs Smoke Test", output);
Assert.Contains("Aspire CLI (https://example.com/install)", output);
Assert.Contains("aspire run", output);
Assert.DoesNotContain("[Aspire CLI](", output);
Assert.DoesNotContain("`aspire run`", output);
Assert.Contains("1. First item", output);
Assert.Contains("Continued explanation.", output);
Assert.Contains("Nested item", output);
Assert.Contains("aspire docs get docs-smoke-test", output);
Assert.Contains("Setting", output);
Assert.Contains("Azure:SubscriptionId", output);
Assert.Contains("Azure__SubscriptionId", output);
Assert.Contains("Target Azure subscription", output);
var headingPos = output.IndexOf("Docs Smoke Test", StringComparison.Ordinal);
var listPos = output.IndexOf("1. First item", StringComparison.Ordinal);
var codePos = output.IndexOf("aspire docs get docs-smoke-test", StringComparison.Ordinal);
var tablePos = output.IndexOf("Azure:SubscriptionId", StringComparison.Ordinal);
Assert.True(headingPos < listPos);
Assert.True(listPos < codePos);
Assert.True(codePos < tablePos);
}
[Fact]
public async Task DocsGetCommand_WithInvalidSlug_ReturnsError()
{
using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.DocsIndexServiceFactory = _ => new TestDocsIndexService();
});
using var provider = services.BuildServiceProvider();
var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
var result = command.Parse("docs get nonexistent-page");
var exitCode = await result.InvokeAsync().DefaultTimeout();
Assert.NotEqual(0, exitCode);
}
}
internal sealed class TestDocsIndexService : IDocsIndexService
{
private readonly IReadOnlyDictionary<string, DocsContent> _documents;
public TestDocsIndexService(IReadOnlyDictionary<string, DocsContent>? documents = null)
{
_documents = documents ?? new Dictionary<string, DocsContent>
{
["redis-integration"] = new()
{
Title = "Redis Integration",
Slug = "redis-integration",
Summary = "Learn how to use Redis",
Content = "# Redis Integration\n\nThis is the Redis integration documentation.",
Sections = ["Getting Started", "Hosting integration", "Client integration"]
}
};
}
public bool IsIndexed => true;
public ValueTask EnsureIndexedAsync(CancellationToken cancellationToken = default)
{
return ValueTask.CompletedTask;
}
public ValueTask<IReadOnlyList<DocsListItem>> ListDocumentsAsync(CancellationToken cancellationToken = default)
{
var docs = _documents.Values
.Select(doc => new DocsListItem { Title = doc.Title, Slug = doc.Slug, Summary = doc.Summary })
.ToList();
docs.Add(new DocsListItem { Title = "PostgreSQL Integration", Slug = "postgresql-integration", Summary = "Learn how to use PostgreSQL" });
docs.Add(new DocsListItem { Title = "Getting Started", Slug = "getting-started", Summary = "Get started with Aspire" });
return ValueTask.FromResult<IReadOnlyList<DocsListItem>>(docs);
}
public ValueTask<IReadOnlyList<DocsSearchResult>> SearchAsync(string query, int topK = 10, CancellationToken cancellationToken = default)
{
var results = new List<DocsSearchResult>
{
new() { Title = "Redis Integration", Slug = "redis-integration", Summary = "Learn how to use Redis", Score = 100.0f, MatchedSection = "Hosting integration" },
new() { Title = "Azure Cache for Redis", Slug = "azure-cache-redis", Summary = "Azure Redis integration", Score = 80.0f, MatchedSection = "Client integration" }
};
return ValueTask.FromResult<IReadOnlyList<DocsSearchResult>>(results.Take(topK).ToList() as IReadOnlyList<DocsSearchResult>);
}
public ValueTask<DocsContent?> GetDocumentAsync(string slug, string? section = null, CancellationToken cancellationToken = default)
{
if (_documents.TryGetValue(slug, out var document))
{
return ValueTask.FromResult<DocsContent?>(document);
}
return ValueTask.FromResult<DocsContent?>(null);
}
}
internal sealed class TestDocsSearchService : IDocsSearchService
{
public Task<DocsSearchResponse?> SearchAsync(string query, int topK = 5, CancellationToken cancellationToken = default)
{
var results = new List<SearchResult>
{
new() { Title = "Redis Integration", Slug = "redis-integration", Content = "Learn how to use Redis", Score = 100.0f, Section = "Hosting integration" },
new() { Title = "Azure Cache for Redis", Slug = "azure-cache-redis", Content = "Azure Redis integration", Score = 80.0f, Section = "Client integration" }
};
return Task.FromResult<DocsSearchResponse?>(new DocsSearchResponse
{
Query = query,
Results = results.Take(topK).ToList()
});
}
}