File: Commands\ResourceCommandHelperTests.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.Json.Nodes;
using Aspire.Cli.Backchannel;
using Aspire.Cli.Commands;
using Aspire.Cli.Tests.TestServices;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Cli.Tests.Commands;
 
public class ResourceCommandHelperTests
{
    [Fact]
    public async Task ExecuteGenericCommandAsync_WithResult_OutputsRawText()
    {
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            ExecuteResourceCommandResult = new ExecuteResourceCommandResponse
            {
                Success = true,
                Value = new ExecuteResourceCommandResult
                {
                    Value = "{\"items\": [\"a\", \"b\"]}",
                    Format = CommandResultFormat.Json
                }
            }
        };
 
        string? capturedRawText = null;
        var interactionService = new TestInteractionService
        {
            DisplayRawTextCallback = (text) => capturedRawText = text
        };
 
        var exitCode = await ResourceCommandHelper.ExecuteGenericCommandAsync(
            connection,
            interactionService,
            NullLogger.Instance,
            "myResource",
            "generate-token",
            arguments: null,
            cancellationToken: CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(0, exitCode);
        Assert.NotNull(capturedRawText);
        // Verify the raw result is passed through without any escaping
        Assert.Equal("{\"items\": [\"a\", \"b\"]}", capturedRawText);
    }
 
    [Fact]
    public async Task ExecuteGenericCommandAsync_WithoutResult_DoesNotCallDisplayMessage()
    {
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            ExecuteResourceCommandResult = new ExecuteResourceCommandResponse { Success = true }
        };
 
        var displayRawTextCalled = false;
        var interactionService = new TestInteractionService
        {
            DisplayRawTextCallback = (_) => displayRawTextCalled = true
        };
 
        var exitCode = await ResourceCommandHelper.ExecuteGenericCommandAsync(
            connection,
            interactionService,
            NullLogger.Instance,
            "myResource",
            "start",
            arguments: null,
            cancellationToken: CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(0, exitCode);
        Assert.False(displayRawTextCalled);
    }
 
    [Fact]
    public async Task ExecuteGenericCommandAsync_ErrorWithResult_OutputsRawText()
    {
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            ExecuteResourceCommandResult = new ExecuteResourceCommandResponse
            {
                Success = false,
                Message = "Validation failed",
                Value = new ExecuteResourceCommandResult
                {
                    Value = "{\"errors\": [\"invalid host\"]}",
                    Format = CommandResultFormat.Json
                }
            }
        };
 
        string? capturedRawText = null;
        var interactionService = new TestInteractionService
        {
            DisplayRawTextCallback = (text) => capturedRawText = text
        };
 
        var exitCode = await ResourceCommandHelper.ExecuteGenericCommandAsync(
            connection,
            interactionService,
            NullLogger.Instance,
            "myResource",
            "validate-config",
            arguments: null,
            cancellationToken: CancellationToken.None).DefaultTimeout();
 
        Assert.NotEqual(0, exitCode);
        Assert.NotNull(capturedRawText);
        Assert.Equal("{\"errors\": [\"invalid host\"]}", capturedRawText);
    }
 
    [Fact]
    public async Task ExecuteGenericCommandAsync_WithArguments_PassesArgumentsToBackchannel()
    {
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            ExecuteResourceCommandResult = new ExecuteResourceCommandResponse { Success = true }
        };
 
        var interactionService = new TestInteractionService();
 
        // Command arguments JSON is expected to be an object, for example: { "selector": "#submit" }.
        var arguments = new JsonObject
        {
            ["selector"] = "#submit"
        };
 
        var exitCode = await ResourceCommandHelper.ExecuteGenericCommandAsync(
            connection,
            interactionService,
            NullLogger.Instance,
            "myResource",
            "click",
            arguments,
            CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(0, exitCode);
        Assert.NotNull(connection.ExecuteResourceCommandArguments);
        Assert.Equal("#submit", connection.ExecuteResourceCommandArguments["selector"]!.GetValue<string>());
        Assert.True(connection.ExecuteResourceCommandOptions?.NonInteractive == true);
    }
 
    [Fact]
    public async Task ExecuteGenericCommandAsync_WithValidationErrors_DisplaysArgumentErrors()
    {
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            ExecuteResourceCommandResult = new ExecuteResourceCommandResponse
            {
                Success = false,
                Message = "Command argument validation failed.",
                ValidationErrors =
                [
                    new ResourceCommandArgumentValidationError
                    {
                        ArgumentName = "target",
                        ErrorMessage = "Target must not be prod."
                    }
                ]
            }
        };
 
        var interactionService = new TestInteractionService();
 
        var exitCode = await ResourceCommandHelper.ExecuteGenericCommandAsync(
            connection,
            interactionService,
            NullLogger.Instance,
            "myResource",
            "validate",
            arguments: null,
            cancellationToken: CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(CliExitCodes.FailedToExecuteResourceCommand, exitCode);
        var error = Assert.Single(interactionService.DisplayedErrors);
        Assert.Contains("Failed to validate command arguments for command 'validate' on resource 'myResource'", error);
        Assert.DoesNotContain("Command argument validation failed.", error);
        Assert.Contains("--target: Target must not be prod.", error);
    }
 
    [Fact]
    public async Task ExecuteGenericCommandAsync_WhenValidationErrorsIsNull_DisplaysCommandError()
    {
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            ExecuteResourceCommandResult = new ExecuteResourceCommandResponse
            {
                Success = false,
                Message = "Command 'ss' not available for resource 'test-resource'.",
                ValidationErrors = null!
            }
        };
 
        var interactionService = new TestInteractionService();
 
        var exitCode = await ResourceCommandHelper.ExecuteGenericCommandAsync(
            connection,
            interactionService,
            NullLogger.Instance,
            "test-resource",
            "ss",
            arguments: null,
            cancellationToken: CancellationToken.None).DefaultTimeout();
 
        Assert.Equal(CliExitCodes.FailedToExecuteResourceCommand, exitCode);
        var error = Assert.Single(interactionService.DisplayedErrors);
        Assert.Contains("Failed to execute command 'ss' on resource 'test-resource'", error);
        Assert.Contains("Command 'ss' not available for resource 'test-resource'.", error);
    }
}