File: Commands\NewCommandLocalTemplateTests.SourceList.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 System.Globalization;
using Aspire.Cli.Configuration;
using Aspire.Cli.Git;
using Aspire.Cli.Interaction;
using Aspire.Cli.Resources;
using Aspire.Cli.Tests.TestServices;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Spectre.Console;
using RootCommand = Aspire.Cli.Commands.RootCommand;
 
namespace Aspire.Cli.Tests.Commands;
 
public partial class NewCommandLocalTemplateTests
{
    private const string SourceListSettings = """
        {
          // Management listing preserves the configured fields, including unfinished sources.
          "unrelated": "not listed",
          "templateSources": {
            "z-git": { "repository": "https://example.test/templates", "path": "templates/api", "ref": "release/17.0" },
            "Local [red]": { "path": "/templates/with [brackets]" },
            "empty": {},
            "git-head": { "repository": "https://example.test/head" },
            "unfinished": { "ref": "main" }
          }
        }
        """;
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task TemplateSourceList_DisplaysGlobalConfigurationWithoutDiscovery(bool json)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        var git = new TestGitCommandRunner();
        var services = CreateServices(workspace, interaction, interactive: true);
        services.AddSingleton<IGitCommandRunner>(git);
        using var provider = services.BuildServiceProvider();
        var settings = provider.GetRequiredService<IConfigurationService>();
        await File.WriteAllTextAsync(settings.GetSettingsFilePath(true), SourceListSettings);
        var localPath = settings.GetSettingsFilePath(false);
        Directory.CreateDirectory(Path.GetDirectoryName(localPath)!);
        const string localSettings = """{"templateSources":{"project-only":{"repository":"https://example.test/project"}}}""";
        await File.WriteAllTextAsync(localPath, localSettings);
 
        var result = await provider.GetRequiredService<RootCommand>()
            .Parse(json ? "template source list --format json" : "template source list").InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, result);
        Assert.Empty(GetGitResolutions(git));
        Assert.Empty(GetGitFetches(git));
        Assert.Empty(interaction.ShownStatuses);
        Assert.Empty(interaction.DisplayedErrors);
        Assert.Empty(interaction.DisplayedMessages);
        Assert.Empty(interaction.DisplayedSuccess);
        Assert.Equal(SourceListSettings, await File.ReadAllTextAsync(settings.GetSettingsFilePath(true)));
        Assert.Equal(localSettings, await File.ReadAllTextAsync(localPath));
        if (json)
        {
            var output = Assert.Single(interaction.DisplayedRawText);
            Assert.Equal(ConsoleOutput.Standard, output.ConsoleOverride);
            Assert.Equal(ConsoleOutput.Error, interaction.Console);
            Assert.Empty(interaction.DisplayedRenderables);
            await Verify(output.Text, "json").UseParameters(json);
        }
        else
        {
            Assert.Empty(interaction.DisplayedRawText);
            using var writer = new StringWriter(CultureInfo.InvariantCulture);
            var console = AnsiConsole.Create(new AnsiConsoleSettings { Out = new AnsiConsoleOutput(writer), Ansi = AnsiSupport.No });
            console.Profile.Capabilities.Ansi = false;
            console.Profile.Width = 160;
            console.Write(Assert.Single(interaction.DisplayedRenderables));
            await Verify(writer.ToString()).UseParameters(json);
        }
    }
 
    [Theory]
    [InlineData(null)]
    [InlineData("{}")]
    [InlineData("""{"templateSources":{}}""")]
    [InlineData("""{"unrelated":true}""")]
    public async Task TemplateSourceList_EmptyJsonIsAnObjectWithoutCreatingOrChangingConfiguration(string? original)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: false).BuildServiceProvider();
        var path = provider.GetRequiredService<IConfigurationService>().GetSettingsFilePath(true);
        if (original is not null)
        {
            await File.WriteAllTextAsync(path, original);
        }
 
        var result = await provider.GetRequiredService<RootCommand>()
            .Parse("template source list --format json").InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, result);
        Assert.Equal(("{}", (ConsoleOutput?)ConsoleOutput.Standard), Assert.Single(interaction.DisplayedRawText));
        Assert.Empty(interaction.DisplayedMessages);
        Assert.Empty(interaction.DisplayedRenderables);
        Assert.Empty(interaction.DisplayedErrors);
        Assert.Equal(original, File.Exists(path) ? await File.ReadAllTextAsync(path) : null);
    }
 
    [Theory]
    [InlineData("")]
    [InlineData(" --format table")]
    public async Task TemplateSourceList_EmptyHumanReadableOutputHasGuidance(string options)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: false).BuildServiceProvider();
 
        var result = await provider.GetRequiredService<RootCommand>()
            .Parse($"template source list{options}").InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.Success, result);
        Assert.Equal(NewCommandStrings.TemplateSourceListEmpty, Assert.Single(interaction.DisplayedMessages).Message);
        Assert.Empty(interaction.DisplayedRawText);
        Assert.Empty(interaction.DisplayedRenderables);
    }
 
    [Theory]
    [InlineData("{")]
    [InlineData("[]")]
    [InlineData("""{"templateSources":null}""")]
    [InlineData("""{"templateSources":42}""")]
    public async Task TemplateSourceList_JsonReadFailureDoesNotReturnEmptySuccess(string original)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: false).BuildServiceProvider();
        var path = provider.GetRequiredService<IConfigurationService>().GetSettingsFilePath(true);
        await File.WriteAllTextAsync(path, original);
 
        var result = await provider.GetRequiredService<RootCommand>()
            .Parse("template source list --format json").InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.InvalidCommand, result);
        Assert.Single(interaction.DisplayedErrors);
        Assert.Equal(ConsoleOutput.Error, interaction.Console);
        Assert.Empty(interaction.DisplayedRawText);
        Assert.Equal(original, await File.ReadAllTextAsync(path));
    }
 
    [Fact]
    public async Task TemplateSourceList_UnreadableConfigurationFailsWithoutJson()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: false).BuildServiceProvider();
        Directory.CreateDirectory(provider.GetRequiredService<IConfigurationService>().GetSettingsFilePath(true));
 
        var result = await provider.GetRequiredService<RootCommand>()
            .Parse("template source list --format json").InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.InvalidCommand, result);
        Assert.Equal(NewCommandStrings.TemplateSourceReadFailed, Assert.Single(interaction.DisplayedErrors));
        Assert.Equal(ConsoleOutput.Error, interaction.Console);
        Assert.Empty(interaction.DisplayedRawText);
    }
 
    [Fact]
    public async Task TemplateSourceList_InvalidFormatIsRejected()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var interaction = new TestInteractionService();
        using var provider = CreateServices(workspace, interaction, interactive: false).BuildServiceProvider();
        var parseResult = provider.GetRequiredService<RootCommand>().Parse("template source list --format yaml");
 
        Assert.NotEmpty(parseResult.Errors);
        Assert.NotEqual(CliExitCodes.Success, await parseResult.InvokeAsync().DefaultTimeout());
        Assert.Empty(interaction.DisplayedRawText);
    }
}