File: Commands\ExtensionInternalCommandTests.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.Text;
using System.Text.Json;
using Aspire.Cli.Backchannel;
using Aspire.Cli.Commands;
using Aspire.Cli.Projects;
using Aspire.Cli.Tests.Utils;
using Microsoft.Extensions.DependencyInjection;
using Aspire.TestUtilities;
 
namespace Aspire.Cli.Tests.Commands;
 
public class ExtensionInternalCommandTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public void ExtensionInternalCommandIsHidden()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<ExtensionInternalCommand>();
        
        Assert.True(command.Hidden);
    }
 
    [Fact]
    public async Task ExtensionInternalCommand_WithHelpArgument_ReturnsZero()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("extension --help");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, exitCode);
    }
 
    [Fact]
    public async Task ExtensionInternalCommand_WithNoSubcommand_ReturnsZero()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("extension");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(ExitCodeConstants.Success, exitCode);
    }
 
    [Fact]
    [QuarantinedTest("https://github.com/dotnet/aspire/issues/12304")]
    public async Task GetAppHostsCommand_WithSingleProject_ReturnsSuccessWithValidJson()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "MyApp.AppHost.csproj"));
        
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = _ => new SingleProjectFileProjectLocator(projectFile);
        });
        var provider = services.BuildServiceProvider();
 
        var output = new StringBuilder();
        var outputWriter = new StringWriter(output);
        Console.SetOut(outputWriter);
 
        try
        {
            var command = provider.GetRequiredService<RootCommand>();
            var result = command.Parse("extension get-apphosts");
 
            var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
            Assert.Equal(ExitCodeConstants.Success, exitCode);
 
            var outputStr = output.ToString().Trim();
            Assert.NotEmpty(outputStr);
 
            // Verify JSON is valid and deserializable
            var searchResult = JsonSerializer.Deserialize<AppHostProjectSearchResultPoco>(
                outputStr, 
                BackchannelJsonSerializerContext.Default.AppHostProjectSearchResultPoco);
            
            Assert.NotNull(searchResult);
            Assert.Equal(projectFile.FullName, searchResult.SelectedProjectFile);
            Assert.Single(searchResult.AllProjectFileCandidates);
            Assert.Equal(projectFile.FullName, searchResult.AllProjectFileCandidates[0]);
        }
        finally
        {
            var standardOutput = new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true };
            Console.SetOut(standardOutput);
        }
    }
 
    [Fact]
    [QuarantinedTest("https://github.com/dotnet/aspire/issues/12300")]
    public async Task GetAppHostsCommand_WithMultipleProjects_ReturnsSuccessWithAllCandidates()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var projectFile1 = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "App1.AppHost.csproj"));
        var projectFile2 = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "App2.AppHost.csproj"));
        
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = _ => new MultipleProjectsProjectLocator([projectFile1, projectFile2]);
        });
        var provider = services.BuildServiceProvider();
 
        var output = new StringBuilder();
        var outputWriter = new StringWriter(output);
        Console.SetOut(outputWriter);
 
        try
        {
            var command = provider.GetRequiredService<RootCommand>();
            var result = command.Parse("extension get-apphosts");
 
            var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
            Assert.Equal(ExitCodeConstants.Success, exitCode);
 
            var outputStr = output.ToString().Trim();
            Assert.NotEmpty(outputStr);
 
            // Verify JSON is valid and deserializable
            var searchResult = JsonSerializer.Deserialize<AppHostProjectSearchResultPoco>(
                outputStr, 
                BackchannelJsonSerializerContext.Default.AppHostProjectSearchResultPoco);
            
            Assert.NotNull(searchResult);
            Assert.Null(searchResult.SelectedProjectFile);
            Assert.Equal(2, searchResult.AllProjectFileCandidates.Count);
            Assert.Contains(projectFile1.FullName, searchResult.AllProjectFileCandidates);
            Assert.Contains(projectFile2.FullName, searchResult.AllProjectFileCandidates);
        }
        finally
        {
            var standardOutput = new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true };
            Console.SetOut(standardOutput);
        }
    }
 
    [Fact]
    public async Task GetAppHostsCommand_WithNoProjects_ReturnsFailureExitCode()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = _ => new NoProjectFileProjectLocator();
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("extension get-apphosts");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(ExitCodeConstants.FailedToFindProject, exitCode);
    }
 
    [Fact]
    public async Task GetAppHostsCommand_WhenProjectLocatorThrows_ReturnsFailureExitCode()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = _ => new ThrowingProjectLocator();
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("extension get-apphosts");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(ExitCodeConstants.FailedToFindProject, exitCode);
    }
 
    [Fact]
    public async Task GetAppHostsCommand_WithHelpArgument_ReturnsZero()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("extension get-apphosts --help");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, exitCode);
    }
 
    // Test helper classes
 
    private sealed class SingleProjectFileProjectLocator : IProjectLocator
    {
        private readonly FileInfo _projectFile;
 
        public SingleProjectFileProjectLocator(FileInfo projectFile)
        {
            _projectFile = projectFile;
        }
 
        public Task<AppHostProjectSearchResult> UseOrFindAppHostProjectFileAsync(
            FileInfo? projectFile, 
            MultipleAppHostProjectsFoundBehavior multipleAppHostProjectsFoundBehavior, 
            bool createSettingsFile, 
            CancellationToken cancellationToken)
        {
            var result = new AppHostProjectSearchResult(_projectFile, [_projectFile]);
            return Task.FromResult(result);
        }
 
        public Task<FileInfo?> UseOrFindAppHostProjectFileAsync(
            FileInfo? projectFile, 
            bool createSettingsFile, 
            CancellationToken cancellationToken)
        {
            return Task.FromResult<FileInfo?>(_projectFile);
        }
 
        public Task<AppHostProjectSearchResult> UseOrFindServiceProjectFileAsync(
            FileInfo? projectFile, 
            MultipleAppHostProjectsFoundBehavior multipleAppHostProjectsFoundBehavior, 
            bool createSettingsFile, 
            CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
 
        public Task<FileInfo?> UseOrFindServiceProjectFileAsync(
            FileInfo? projectFile, 
            bool createSettingsFile, 
            CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
 
        public Task<FileInfo?> UseOrFindSolutionFileAsync(
            FileInfo? solutionFile,
            bool createSettingsFile,
            CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
        
        public Task<IReadOnlyList<FileInfo>> FindExecutableProjectsAsync(string searchDirectory, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
    }
 
    private sealed class MultipleProjectsProjectLocator : IProjectLocator
    {
        private readonly List<FileInfo> _projectFiles;
 
        public MultipleProjectsProjectLocator(List<FileInfo> projectFiles)
        {
            _projectFiles = projectFiles;
        }
 
        public Task<AppHostProjectSearchResult> UseOrFindAppHostProjectFileAsync(
            FileInfo? projectFile, 
            MultipleAppHostProjectsFoundBehavior multipleAppHostProjectsFoundBehavior, 
            bool createSettingsFile, 
            CancellationToken cancellationToken)
        {
            var result = new AppHostProjectSearchResult(null, _projectFiles);
            return Task.FromResult(result);
        }
 
        public Task<FileInfo?> UseOrFindAppHostProjectFileAsync(
            FileInfo? projectFile, 
            bool createSettingsFile, 
            CancellationToken cancellationToken)
        {
            return Task.FromResult<FileInfo?>(null);
        }
 
        public Task<AppHostProjectSearchResult> UseOrFindServiceProjectFileAsync(
            FileInfo? projectFile, 
            MultipleAppHostProjectsFoundBehavior multipleAppHostProjectsFoundBehavior, 
            bool createSettingsFile, 
            CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
 
        public Task<FileInfo?> UseOrFindServiceProjectFileAsync(
            FileInfo? projectFile, 
            bool createSettingsFile, 
            CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
 
        public Task<FileInfo?> UseOrFindSolutionFileAsync(
            FileInfo? solutionFile,
            bool createSettingsFile,
            CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
        
        public Task<IReadOnlyList<FileInfo>> FindExecutableProjectsAsync(string searchDirectory, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
    }
 
    private sealed class NoProjectFileProjectLocator : IProjectLocator
    {
        public Task<AppHostProjectSearchResult> UseOrFindAppHostProjectFileAsync(
            FileInfo? projectFile, 
            MultipleAppHostProjectsFoundBehavior multipleAppHostProjectsFoundBehavior, 
            bool createSettingsFile, 
            CancellationToken cancellationToken)
        {
            throw new ProjectLocatorException("No AppHost project found.");
        }
 
        public Task<FileInfo?> UseOrFindAppHostProjectFileAsync(
            FileInfo? projectFile, 
            bool createSettingsFile, 
            CancellationToken cancellationToken)
        {
            throw new ProjectLocatorException("No AppHost project found.");
        }
 
        public Task<AppHostProjectSearchResult> UseOrFindServiceProjectFileAsync(
            FileInfo? projectFile, 
            MultipleAppHostProjectsFoundBehavior multipleAppHostProjectsFoundBehavior, 
            bool createSettingsFile, 
            CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
 
        public Task<FileInfo?> UseOrFindServiceProjectFileAsync(
            FileInfo? projectFile, 
            bool createSettingsFile, 
            CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
 
        public Task<FileInfo?> UseOrFindSolutionFileAsync(
            FileInfo? solutionFile,
            bool createSettingsFile,
            CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
        
        public Task<IReadOnlyList<FileInfo>> FindExecutableProjectsAsync(string searchDirectory, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
    }
 
    private sealed class ThrowingProjectLocator : IProjectLocator
    {
        public Task<AppHostProjectSearchResult> UseOrFindAppHostProjectFileAsync(
            FileInfo? projectFile, 
            MultipleAppHostProjectsFoundBehavior multipleAppHostProjectsFoundBehavior, 
            bool createSettingsFile, 
            CancellationToken cancellationToken)
        {
            throw new InvalidOperationException("Something went wrong");
        }
 
        public Task<FileInfo?> UseOrFindAppHostProjectFileAsync(
            FileInfo? projectFile, 
            bool createSettingsFile, 
            CancellationToken cancellationToken)
        {
            throw new InvalidOperationException("Something went wrong");
        }
 
        public Task<AppHostProjectSearchResult> UseOrFindServiceProjectFileAsync(
            FileInfo? projectFile, 
            MultipleAppHostProjectsFoundBehavior multipleAppHostProjectsFoundBehavior, 
            bool createSettingsFile, 
            CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
 
        public Task<FileInfo?> UseOrFindServiceProjectFileAsync(
            FileInfo? projectFile, 
            bool createSettingsFile, 
            CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
 
        public Task<FileInfo?> UseOrFindSolutionFileAsync(
            FileInfo? solutionFile,
            bool createSettingsFile,
            CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
        
        public Task<IReadOnlyList<FileInfo>> FindExecutableProjectsAsync(string searchDirectory, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
    }
}