File: Commands\NewCommandTests.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.Certificates;
using Aspire.Cli.Commands;
using Aspire.Cli.Interaction;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
 
namespace Aspire.Cli.Tests.Commands;
 
public class NewCommandTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public async Task NewCommandWithHelpArgumentReturnsZero()
    {
        var services = CliTestHelper.CreateServiceCollection(outputHelper);
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("new --help");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, exitCode);
    }
 
    [Fact]
    public async Task NewCommandInteractiveFlowSmokeTest()
    {
        var services = CliTestHelper.CreateServiceCollection(outputHelper, options => {
 
            // Set of options that we'll give when prompted.
            options.NewCommandPrompterFactory = (sp) =>
            {
                var interactionService = sp.GetRequiredService<IInteractionService>();
                return new TestNewCommandPrompter(interactionService);
            };
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner();
                runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, options, cancellationToken) =>
                {
                    var package = new NuGetPackage()
                    {
                        Id = "Aspire.ProjectTemplates",
                        Source = "nuget",
                        Version = "9.2.0"
                    };
 
                    return (
                        0, // Exit code.
                        new NuGetPackage[] { package } // Single package.
                        );
                };
 
                return runner;
            };
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<NewCommand>();
        var result = command.Parse("new");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, exitCode);
    }
 
    [Fact]
    public async Task NewCommandDerivesOutputPathFromProjectNameForStarterTemplate()
    {
        var services = CliTestHelper.CreateServiceCollection(outputHelper, options => {
 
            // Set of options that we'll give when prompted.
            options.NewCommandPrompterFactory = (sp) =>
            {
                var interactionService = sp.GetRequiredService<IInteractionService>();
                var prompter = new TestNewCommandPrompter(interactionService);
 
                prompter.PromptForTemplateCallback = (templates) =>
                {
                    return templates.Single(t => t.TemplateName == "aspire-starter");
                };
 
                prompter.PromptForProjectNameCallback = (defaultName) =>
                {
                    return "CustomName";
                };
 
                prompter.PromptForOutputPathCallback = (path) =>
                {
                    Assert.Equal("./CustomName", path);
                    return path;
                };
 
                return prompter;
            };
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner();
                runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, options, cancellationToken) =>
                {
                    var package = new NuGetPackage()
                    {
                        Id = "Aspire.ProjectTemplates",
                        Source = "nuget",
                        Version = "9.2.0"
                    };
 
                    return (
                        0, // Exit code.
                        new NuGetPackage[] { package } // Single package.
                        );
                };
 
                return runner;
            };
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<NewCommand>();
        var result = command.Parse("new");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, exitCode);
    }
 
    [Fact]
    public async Task NewCommandPromptsForProjectTemplateIfInvalidTemplateSpecified()
    {
        var tcs = new TaskCompletionSource();
 
        var services = CliTestHelper.CreateServiceCollection(outputHelper, options => {
 
            // Set of options that we'll give when prompted.
            options.NewCommandPrompterFactory = (sp) =>
            {
                var interactionService = sp.GetRequiredService<IInteractionService>();
                var prompter = new TestNewCommandPrompter(interactionService);
                prompter.PromptForTemplateCallback = (templates) => {
                    tcs.SetResult();
                    return templates[0];
                };
 
                return prompter;
            };
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner();
                runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, options, cancellationToken) =>
                {
                    var package = new NuGetPackage()
                    {
                        Id = "Aspire.ProjectTemplates",
                        Source = "nuget",
                        Version = "9.2.0"
                    };
 
                    return (
                        0, // Exit code.
                        new NuGetPackage[] { package } // Single package.
                        );
                };
 
                return runner;
            };
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<NewCommand>();
        var result = command.Parse("new notavalidtemplate");
 
        var pendingNewCommand = result.InvokeAsync();
 
        // This basically waits for the prompt to select the project
        // template to be shown. If it isn't shown then this test will
        // fail witha timeout.
        await tcs.Task.WaitAsync(CliTestConstants.DefaultTimeout);
        var exitCode = await pendingNewCommand;
 
        // Success because the default behavior of the prompter will
        // result in the new command completing and the stub
        // for the DotNetCliRunner will return success for all the
        // commands.
        Assert.Equal(ExitCodeConstants.Success, exitCode);
    }
 
    [Fact]
    public async Task NewCommandIgnoresCaseWhenSearchingForTemplateName()
    {
        var services = CliTestHelper.CreateServiceCollection(outputHelper, options => {
 
            // Set of options that we'll give when prompted.
            options.NewCommandPrompterFactory = (sp) =>
            {
                var interactionService = sp.GetRequiredService<IInteractionService>();
                return new TestNewCommandPrompter(interactionService);
            };
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner();
                runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, options, cancellationToken) =>
                {
                    var package = new NuGetPackage()
                    {
                        Id = "Aspire.ProjectTemplates",
                        Source = "nuget",
                        Version = "9.2.0"
                    };
 
                    return (
                        0, // Exit code.
                        new NuGetPackage[] { package } // Single package.
                        );
                };
 
                runner.NewProjectAsyncCallback = (templateName, name, outputPath, options, cancellationToken) =>
                {
                    // This is the template name that we expect to be passed
                    // to the new command.
                    Assert.Equal("aspire-apphost", templateName);
                    return 0; // Success
                };
 
                return runner;
            };
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<NewCommand>();
        var result = command.Parse("new ASPIRE-APPHOST");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(ExitCodeConstants.Success, exitCode);
    }
 
    [Fact]
    public async Task NewCommandOrdersTemplatePackageVersionsCorrectly()
    {
        IEnumerable<NuGetPackage>? promptedPackages = null;
 
        var services = CliTestHelper.CreateServiceCollection(outputHelper, options => {
 
            // Set of options that we'll give when prompted.
            options.NewCommandPrompterFactory = (sp) =>
            {
                var interactionService = sp.GetRequiredService<IInteractionService>();
                var prompter =  new TestNewCommandPrompter(interactionService);
 
                prompter.PromptForTemplatesVersionCallback = (packages) =>
                {
                    promptedPackages = packages;
                    return promptedPackages.First();
                };
 
                return prompter;
            };
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner();
                runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, options, cancellationToken) =>
                {
                    var package92 = new NuGetPackage()
                    {
                        Id = "Aspire.ProjectTemplates",
                        Source = "othernuget",
                        Version = "9.2.0"
                    };
 
                    var package93 = new NuGetPackage()
                    {
                        Id = "Aspire.ProjectTemplates",
                        Source = "nuget",
                        Version = "9.3.0"
                    };
 
                    return (
                        0, // Exit code.
                        new NuGetPackage[] { package92, package93 }
                        );
                };
 
                return runner;
            };
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<NewCommand>();
        var result = command.Parse("new");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, exitCode);
        Assert.NotNull(promptedPackages);
        Assert.Collection(
            promptedPackages,
            package => Assert.Equal("9.3.0", package.Version),
            package => Assert.Equal("9.2.0", package.Version)
        );
    }
 
    [Fact]
    public async Task NewCommandOrdersTemplatePackageVersionsCorrectlyWithPrerelease()
    {
        IEnumerable<NuGetPackage>? promptedPackages = null;
 
        var services = CliTestHelper.CreateServiceCollection(outputHelper, options => {
 
            // Set of options that we'll give when prompted.
            options.NewCommandPrompterFactory = (sp) =>
            {
                var interactionService = sp.GetRequiredService<IInteractionService>();
                var prompter =  new TestNewCommandPrompter(interactionService);
 
                prompter.PromptForTemplatesVersionCallback = (packages) =>
                {
                    promptedPackages = packages;
                    return promptedPackages.First();
                };
 
                return prompter;
            };
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner();
                runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, options, cancellationToken) =>
                {
                    var package92 = new NuGetPackage()
                    {
                        Id = "Aspire.ProjectTemplates",
                        Source = "othernuget",
                        Version = "9.2.0"
                    };
 
                    var package94 = new NuGetPackage()
                    {
                        Id = "Aspire.ProjectTemplates",
                        Source = "internalfeed",
                        Version = "9.4.0-preview.1234"
                    };
 
                    var package93 = new NuGetPackage()
                    {
                        Id = "Aspire.ProjectTemplates",
                        Source = "nuget",
                        Version = "9.3.0"
                    };
 
                    return (
                        0, // Exit code.
                        new NuGetPackage[] { package92, package94, package93 }
                        );
                };
 
                return runner;
            };
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<NewCommand>();
        var result = command.Parse("new");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, exitCode);
        Assert.NotNull(promptedPackages);
        Assert.Collection(
            promptedPackages,
            package => Assert.Equal("9.4.0-preview.1234", package.Version),
            package => Assert.Equal("9.3.0", package.Version),
            package => Assert.Equal("9.2.0", package.Version)
        );
    }
 
    [Fact]
    public async Task NewCommandDoesNotPromptForProjectNameIfSpecifiedOnCommandLine()
    {
        var promptedForName = false;
 
        var services = CliTestHelper.CreateServiceCollection(outputHelper, options => {
 
            // Set of options that we'll give when prompted.
            options.NewCommandPrompterFactory = (sp) =>
            {
                var interactionService = sp.GetRequiredService<IInteractionService>();
                var prompter = new TestNewCommandPrompter(interactionService);
 
                prompter.PromptForProjectNameCallback = (defaultName) =>
                {
                    promptedForName = true;
                    throw new InvalidOperationException("This should not be called");
                };
 
                return prompter;
            };
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner();
                runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, options, cancellationToken) =>
                {
                    var package = new NuGetPackage()
                    {
                        Id = "Aspire.ProjectTemplates",
                        Source = "nuget",
                        Version = "9.2.0"
                    };
 
                    return (
                        0, // Exit code.
                        new NuGetPackage[] { package } // Single package.
                        );
                };
 
                return runner;
            };
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<NewCommand>();
        var result = command.Parse("new --name MyApp");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, exitCode);
        Assert.False(promptedForName);
    }
 
    [Fact]
    public async Task NewCommandDoesNotPromptForOutputPathIfSpecifiedOnCommandLine()
    {
        bool promptedForPath = false;
 
        var services = CliTestHelper.CreateServiceCollection(outputHelper, options => {
 
            // Set of options that we'll give when prompted.
            options.NewCommandPrompterFactory = (sp) =>
            {
                var interactionService = sp.GetRequiredService<IInteractionService>();
                var prompter = new TestNewCommandPrompter(interactionService);
 
                prompter.PromptForOutputPathCallback = (path) =>
                {
                    promptedForPath = true;
                    throw new InvalidOperationException("This should not be called");
                };
 
                return prompter;
            };
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner();
                runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, options, cancellationToken) =>
                {
                    var package = new NuGetPackage()
                    {
                        Id = "Aspire.ProjectTemplates",
                        Source = "nuget",
                        Version = "9.2.0"
                    };
 
                    return (
                        0, // Exit code.
                        new NuGetPackage[] { package } // Single package.
                        );
                };
 
                return runner;
            };
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<NewCommand>();
        var result = command.Parse("new --output notsrc");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, exitCode);
        Assert.False(promptedForPath);
    }
 
    [Fact]
    public async Task NewCommandDoesNotPromptForTemplateIfSpecifiedOnCommandLine()
    {
        bool promptedForTemplate = false;
 
        var services = CliTestHelper.CreateServiceCollection(outputHelper, options => {
 
            // Set of options that we'll give when prompted.
            options.NewCommandPrompterFactory = (sp) =>
            {
                var interactionService = sp.GetRequiredService<IInteractionService>();
                var prompter = new TestNewCommandPrompter(interactionService);
 
                prompter.PromptForTemplateCallback = (path) =>
                {
                    promptedForTemplate = true;
                    throw new InvalidOperationException("This should not be called");
                };
 
                return prompter;
            };
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner();
                runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, options, cancellationToken) =>
                {
                    var package = new NuGetPackage()
                    {
                        Id = "Aspire.ProjectTemplates",
                        Source = "nuget",
                        Version = "9.2.0"
                    };
 
                    return (
                        0, // Exit code.
                        new NuGetPackage[] { package } // Single package.
                        );
                };
 
                return runner;
            };
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<NewCommand>();
        var result = command.Parse("new aspire-starter");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, exitCode);
        Assert.False(promptedForTemplate);
    }
 
    [Fact]
    public async Task NewCommandDoesNotPromptForTemplateVersionIfSpecifiedOnCommandLine()
    {
        bool promptedForTemplateVersion = false;
 
        var services = CliTestHelper.CreateServiceCollection(outputHelper, options => {
 
            // Set of options that we'll give when prompted.
            options.NewCommandPrompterFactory = (sp) =>
            {
                var interactionService = sp.GetRequiredService<IInteractionService>();
                var prompter = new TestNewCommandPrompter(interactionService);
 
                prompter.PromptForTemplatesVersionCallback = (packages) =>
                {
                    promptedForTemplateVersion = true;
                    throw new InvalidOperationException("This should not be called");
                };
 
                return prompter;
            };
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner();
                runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, options, cancellationToken) =>
                {
                    var package = new NuGetPackage()
                    {
                        Id = "Aspire.ProjectTemplates",
                        Source = "nuget",
                        Version = "9.2.0"
                    };
 
                    return (
                        0, // Exit code.
                        new NuGetPackage[] { package } // Single package.
                        );
                };
 
                return runner;
            };
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<NewCommand>();
        var result = command.Parse("new --version 9.2.0");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, exitCode);
        Assert.False(promptedForTemplateVersion);
    }
 
    [Fact]
    public async Task NewCommand_WhenCertificateServiceThrows_ReturnsNonZeroExitCode()
    {
        var services = CliTestHelper.CreateServiceCollection(outputHelper, options =>
        {
            options.NewCommandPrompterFactory = (sp) => {
                var interactionService = sp.GetRequiredService<IInteractionService>();
                var prompter = new TestNewCommandPrompter(interactionService);
                return prompter;
            };
            options.CertificateServiceFactory = _ => new ThrowingCertificateService();
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner();
                runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, options, cancellationToken) =>
                {
                    var package = new NuGetPackage()
                    {
                        Id = "Aspire.ProjectTemplates",
                        Source = "nuget",
                        Version = "9.2.0"
                    };
 
                    return (
                        0, // Exit code.
                        new NuGetPackage[] { package } // Single package.
                        );
                };
 
                runner.InstallTemplateAsyncCallback = (packageName, version, nugetSource, force, options, cancellationToken) =>
                {
                    return (0, version); // Success, return the template version
                };
 
                runner.NewProjectAsyncCallback = (templateName, name, outputPath, options, cancellationToken) =>
                {
                    return 0; // Success
                };
 
                return runner;
            };
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("new");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(ExitCodeConstants.FailedToTrustCertificates, exitCode);
    }
 
    private sealed class ThrowingCertificateService : ICertificateService
    {
        public Task EnsureCertificatesTrustedAsync(IDotNetCliRunner runner, CancellationToken cancellationToken)
        {
            throw new CertificateServiceException("Failed to trust certificates");
        }
    }
}
 
internal sealed class TestNewCommandPrompter(IInteractionService interactionService) : NewCommandPrompter(interactionService)
{
    public Func<IEnumerable<NuGetPackage>, NuGetPackage>? PromptForTemplatesVersionCallback { get; set; }
    public Func<(string TemplateName, string TemplateDescription, Func<string, string> PathDeriver)[], (string TemplateName, string TemplateDescription, Func<string, string> PathDeriver)>? PromptForTemplateCallback { get; set; }
    public Func<string, string>? PromptForProjectNameCallback { get; set; }
    public Func<string, string>? PromptForOutputPathCallback { get; set; }
 
    public override Task<(string TemplateName, string TemplateDescription, Func<string, string> PathDeriver)> PromptForTemplateAsync((string TemplateName, string TemplateDescription, Func<string, string> PathDeriver)[] validTemplates, CancellationToken cancellationToken)
    {
        return PromptForTemplateCallback switch
        {
            { } callback => Task.FromResult(callback(validTemplates)),
            _ => Task.FromResult(validTemplates[0]) // If no callback is provided just accept the first template.
        };
    }
 
    public override Task<string> PromptForProjectNameAsync(string defaultName, CancellationToken cancellationToken)
    {
        return PromptForProjectNameCallback switch
        {
            { } callback => Task.FromResult(callback(defaultName)),
            _ => Task.FromResult(defaultName) // If no callback is provided just accept the default.
        };
    }
 
    public override Task<string> PromptForOutputPath(string path, CancellationToken cancellationToken)
    {
        return PromptForOutputPathCallback switch
        {
            { } callback => Task.FromResult(callback(path)),
            _ => Task.FromResult(path) // If no callback is provided just accept the default.
        };
    }
 
    public override Task<NuGetPackage> PromptForTemplatesVersionAsync(IEnumerable<NuGetPackage> candidatePackages, CancellationToken cancellationToken)
    {
        return PromptForTemplatesVersionCallback switch
        {
            { } callback => Task.FromResult(callback(candidatePackages)),
            _ => Task.FromResult(candidatePackages.First()) // If no callback is provided just accept the first package.
        };
    }
}