File: Commands\DestroyCommandTests.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.Runtime.CompilerServices;
using Aspire.Cli.Backchannel;
using Aspire.Cli.Commands;
using Aspire.Cli.Interaction;
using Aspire.Cli.Resources;
using Aspire.Cli.Tests.Utils;
using Aspire.Cli.Tests.TestServices;
using Microsoft.Extensions.DependencyInjection;
using Aspire.Cli.Utils;
using Microsoft.AspNetCore.InternalTesting;
 
namespace Aspire.Cli.Tests.Commands;
 
public class DestroyCommandTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public async Task DestroyCommandWithHelpArgumentReturnsZero()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        using var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("destroy --help");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
        Assert.Equal(0, exitCode);
    }
 
    [Fact]
    public async Task DestroyCommandFailsWithInvalidProjectFile()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    {
                        return (1, false, null);
                    }
                };
                return runner;
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var result = command.Parse("destroy --apphost invalid.csproj");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.FailedToFindProject, exitCode);
    }
 
    [Fact]
    public async Task DestroyCommandPassesCorrectStepArgument()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = (sp) => new TestProjectLocator();
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
 
                    GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    {
                        return (0, true, VersionHelper.GetDefaultTemplateVersion());
                    },
 
                    RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                    {
                        Assert.True(options.NoLaunchProfile);
 
                        Assert.Contains("--operation", args);
                        Assert.Contains("publish", args);
                        Assert.Contains("--step", args);
                        Assert.Contains("destroy", args);
 
                        var destroyCompleted = new TaskCompletionSource();
                        var backchannel = new TestAppHostBackchannel
                        {
                            RequestStopAsyncCalled = destroyCompleted
                        };
                        backchannelCompletionSource?.SetResult(backchannel);
                        await destroyCompleted.Task.DefaultTimeout();
                        return 0;
                    }
                };
 
                return runner;
            };
 
            options.PublishCommandPrompterFactory = (sp) =>
            {
                var interactionService = sp.GetRequiredService<IInteractionService>();
                return new TestDeployCommandPrompter(interactionService);
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var result = command.Parse("destroy");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(0, exitCode);
    }
 
    [Theory]
    [InlineData("destroy --non-interactive")]
    [InlineData("--non-interactive destroy")]
    public async Task DestroyCommandFailsFastWhenNonInteractiveWithoutYes(string commandLine)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var appHostStarted = false;
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = (sp) => new TestProjectLocator();
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    RunAsyncCallback = (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                    {
                        appHostStarted = true;
                        return Task.FromResult(0);
                    }
                };
 
                return runner;
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var result = command.Parse(commandLine);
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.InvalidCommand, exitCode);
        var error = Assert.Single(result.Errors);
        Assert.Equal(string.Format(System.Globalization.CultureInfo.CurrentCulture, SharedCommandStrings.NonInteractiveRequiresYesFormat, "destroy"), error.Message);
        Assert.False(appHostStarted);
    }
 
    [Theory]
    [InlineData("destroy --yes")]
    [InlineData("destroy --non-interactive --yes")]
    [InlineData("--non-interactive destroy --yes")]
    public async Task DestroyCommandForwardsYesFlag(string commandLine)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = (sp) => new TestProjectLocator();
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
 
                    GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    {
                        return (0, true, VersionHelper.GetDefaultTemplateVersion());
                    },
 
                    RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                    {
                        Assert.Contains("--yes", args);
                        Assert.Contains("true", args);
                        Assert.Contains("--step", args);
                        Assert.Contains("destroy", args);
 
                        var destroyCompleted = new TaskCompletionSource();
                        var backchannel = new TestAppHostBackchannel
                        {
                            RequestStopAsyncCalled = destroyCompleted
                        };
                        backchannelCompletionSource?.SetResult(backchannel);
                        await destroyCompleted.Task.DefaultTimeout();
                        return 0;
                    }
                };
 
                return runner;
            };
 
            options.PublishCommandPrompterFactory = (sp) =>
            {
                var interactionService = sp.GetRequiredService<IInteractionService>();
                return new TestDeployCommandPrompter(interactionService);
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var result = command.Parse(commandLine);
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(0, exitCode);
    }
 
    [Fact]
    public async Task DestroyCommandIncludesOutputPathWhenSpecified()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var testOutputPath = Path.Combine(Path.GetTempPath(), "test-destroy");
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = (sp) => new TestProjectLocator();
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
 
                    GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    {
                        return (0, true, VersionHelper.GetDefaultTemplateVersion());
                    },
 
                    RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                    {
                        Assert.Contains("--output-path", args);
                        Assert.Contains(testOutputPath, args);
                        Assert.Contains("--step", args);
                        Assert.Contains("destroy", args);
 
                        var destroyCompleted = new TaskCompletionSource();
                        var backchannel = new TestAppHostBackchannel
                        {
                            RequestStopAsyncCalled = destroyCompleted
                        };
                        backchannelCompletionSource?.SetResult(backchannel);
                        await destroyCompleted.Task.DefaultTimeout();
                        return 0;
                    }
                };
 
                return runner;
            };
 
            options.PublishCommandPrompterFactory = (sp) =>
            {
                var interactionService = sp.GetRequiredService<IInteractionService>();
                return new TestDeployCommandPrompter(interactionService);
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var result = command.Parse($"destroy --output-path {testOutputPath}");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(0, exitCode);
    }
 
    [Fact]
    public async Task DestroyCommandReturnsNonZeroExitCodeWhenDestroyActivitiesFail()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.ProjectLocatorFactory = (sp) => new TestProjectLocator();
 
            options.DotNetCliRunnerFactory = (sp) =>
            {
                var runner = new TestDotNetCliRunner
                {
                    BuildAsyncCallback = (projectFile, noRestore, options, cancellationToken) => 0,
 
                    GetAppHostInformationAsyncCallback = (projectFile, options, cancellationToken) =>
                    {
                        return (0, true, VersionHelper.GetDefaultTemplateVersion());
                    },
 
                    RunAsyncCallback = async (projectFile, watch, noBuild, noRestore, args, env, backchannelCompletionSource, options, cancellationToken) =>
                    {
                        var destroyCompleted = new TaskCompletionSource();
                        var backchannel = new TestAppHostBackchannel
                        {
                            RequestStopAsyncCalled = destroyCompleted,
                            GetPublishingActivitiesAsyncCallback = GetFailedDestroyActivities
                        };
                        backchannelCompletionSource?.SetResult(backchannel);
                        await destroyCompleted.Task.DefaultTimeout();
                        return 0;
                    }
                };
 
                return runner;
            };
 
            options.PublishCommandPrompterFactory = (sp) =>
            {
                var interactionService = sp.GetRequiredService<IInteractionService>();
                return new TestDeployCommandPrompter(interactionService);
            };
        });
 
        using var provider = services.BuildServiceProvider();
        var command = provider.GetRequiredService<RootCommand>();
 
        var result = command.Parse("destroy --yes");
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(CliExitCodes.FailedToBuildArtifacts, exitCode);
 
        static async IAsyncEnumerable<PublishingActivity> GetFailedDestroyActivities([EnumeratorCancellation] CancellationToken cancellationToken)
        {
            await Task.Yield();
 
            yield return new PublishingActivity
            {
                Type = PublishingActivityTypes.Step,
                Data = new PublishingActivityData
                {
                    Id = "destroy-step",
                    StatusText = "Destroying resources",
                    CompletionState = CompletionStates.InProgress,
                    StepId = null
                }
            };
 
            yield return new PublishingActivity
            {
                Type = PublishingActivityTypes.Task,
                Data = new PublishingActivityData
                {
                    Id = "destroy-compose",
                    StatusText = "Stopping containers",
                    CompletionState = CompletionStates.InProgress,
                    StepId = "destroy-step"
                }
            };
 
            yield return new PublishingActivity
            {
                Type = PublishingActivityTypes.Task,
                Data = new PublishingActivityData
                {
                    Id = "destroy-compose",
                    StatusText = "Stopping containers failed",
                    CompletionMessage = "Failed to stop containers",
                    CompletionState = CompletionStates.CompletedWithError,
                    StepId = "destroy-step"
                }
            };
 
            // Simulate publish complete with error
            yield return new PublishingActivity
            {
                Type = PublishingActivityTypes.PublishComplete,
                Data = new PublishingActivityData
                {
                    Id = "publish-complete",
                    StatusText = "Deployment completed with errors",
                    CompletionState = CompletionStates.CompletedWithError,
                    StepId = null
                }
            };
        }
    }
}