File: Commands\PsCommandTests.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;
using Aspire.Cli.Backchannel;
using Aspire.Cli.Commands;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.InternalTesting;
 
namespace Aspire.Cli.Tests.Commands;
 
public class PsCommandTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public async Task PsCommand_Help_Works()
    {
        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("ps --help");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(ExitCodeConstants.Success, exitCode);
    }
 
    [Fact]
    public async Task PsCommand_WhenNoAppHostRunning_ReturnsSuccess()
    {
        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("ps");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        // ps should succeed even with no running AppHosts (just shows empty list)
        Assert.Equal(ExitCodeConstants.Success, exitCode);
    }
 
    [Theory]
    [InlineData("json")]
    [InlineData("Json")]
    [InlineData("JSON")]
    public async Task PsCommand_FormatOption_IsCaseInsensitive(string format)
    {
        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($"ps --format {format}");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(ExitCodeConstants.Success, exitCode);
    }
 
    [Theory]
    [InlineData("table")]
    [InlineData("Table")]
    [InlineData("TABLE")]
    public async Task PsCommand_FormatOption_AcceptsTable(string format)
    {
        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($"ps --format {format}");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(ExitCodeConstants.Success, exitCode);
    }
 
    [Fact]
    public async Task PsCommand_FormatOption_RejectsInvalidValue()
    {
        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("ps --format invalid");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.NotEqual(ExitCodeConstants.Success, exitCode);
    }
 
    [Fact]
    public async Task PsCommand_JsonFormat_ReturnsValidJson()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var textWriter = new TestOutputTextWriter(outputHelper);
 
        var monitor = new TestAuxiliaryBackchannelMonitor();
        var connection1 = new TestAppHostAuxiliaryBackchannel
        {
            IsInScope = true,
            AppHostInfo = new AppHostInformation
            {
                AppHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "App1", "App1.AppHost.csproj"),
                ProcessId = 1234,
                CliProcessId = 5678
            },
            DashboardUrlsState = new DashboardUrlsState
            {
                BaseUrlWithLoginToken = "http://localhost:18888/login?t=abc123"
            }
        };
        var connection2 = new TestAppHostAuxiliaryBackchannel
        {
            Hash = "test-hash-2",
            SocketPath = "/tmp/test2.sock",
            IsInScope = true,
            AppHostInfo = new AppHostInformation
            {
                AppHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "App2", "App2.AppHost.csproj"),
                ProcessId = 9012
            }
        };
        monitor.AddConnection("hash1", "socket.hash1", connection1);
        monitor.AddConnection("hash2", "socket.hash2", connection2);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.OutputTextWriter = textWriter;
            options.AuxiliaryBackchannelMonitorFactory = _ => monitor;
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("ps --format json");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(ExitCodeConstants.Success, exitCode);
 
        var jsonOutput = string.Join(string.Empty, textWriter.Logs);
 
        var appHosts = JsonSerializer.Deserialize(jsonOutput, PsCommandJsonContext.RelaxedEscaping.ListAppHostDisplayInfo);
        Assert.NotNull(appHosts);
 
        Assert.Collection(appHosts.OrderBy(a => a.AppHostPid),
            first =>
            {
                Assert.EndsWith("App1.AppHost.csproj", first.AppHostPath);
                Assert.Equal(1234, first.AppHostPid);
                Assert.Equal(5678, first.CliPid);
                Assert.Equal("http://localhost:18888/login?t=abc123", first.DashboardUrl);
            },
            second =>
            {
                Assert.EndsWith("App2.AppHost.csproj", second.AppHostPath);
                Assert.Equal(9012, second.AppHostPid);
                Assert.Null(second.CliPid);
                Assert.Null(second.DashboardUrl);
            });
    }
 
    [Fact]
    public async Task PsCommand_JsonFormat_NoResults_WritesEmptyArrayToStdout()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var textWriter = new TestOutputTextWriter(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.OutputTextWriter = textWriter;
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("ps --format json");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(ExitCodeConstants.Success, exitCode);
 
        var json = string.Join(string.Empty, textWriter.Logs);
        var document = JsonDocument.Parse(json);
        Assert.Equal(JsonValueKind.Array, document.RootElement.ValueKind);
        Assert.Equal(0, document.RootElement.GetArrayLength());
    }
 
    [Fact]
    public async Task PsCommand_ResourcesOption_IncludesResourcesInJsonOutput()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var textWriter = new TestOutputTextWriter(outputHelper);
 
        var monitor = new TestAuxiliaryBackchannelMonitor();
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            IsInScope = true,
            AppHostInfo = new AppHostInformation
            {
                AppHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "App1", "App1.AppHost.csproj"),
                ProcessId = 1234,
                CliProcessId = 5678
            },
            DashboardUrlsState = new DashboardUrlsState
            {
                BaseUrlWithLoginToken = "http://localhost:18888/login?t=abc123"
            },
            ResourceSnapshots =
            [
                new ResourceSnapshot
                {
                    Name = "apiservice",
                    DisplayName = "apiservice",
                    ResourceType = "Project",
                    State = "Running",
                    StateStyle = "success",
                    Urls =
                    [
                        new ResourceSnapshotUrl { Name = "https", Url = "https://localhost:7001" }
                    ]
                },
                new ResourceSnapshot
                {
                    Name = "redis",
                    DisplayName = "redis",
                    ResourceType = "Container",
                    State = "Running",
                    StateStyle = "success"
                }
            ]
        };
        monitor.AddConnection("hash1", "socket.hash1", connection);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.OutputTextWriter = textWriter;
            options.AuxiliaryBackchannelMonitorFactory = _ => monitor;
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("ps --format json --resources");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(ExitCodeConstants.Success, exitCode);
 
        var jsonOutput = string.Join(string.Empty, textWriter.Logs);
        var appHosts = JsonSerializer.Deserialize(jsonOutput, PsCommandJsonContext.RelaxedEscaping.ListAppHostDisplayInfo);
        Assert.NotNull(appHosts);
        Assert.Single(appHosts);
 
        var appHost = appHosts[0];
        Assert.NotNull(appHost.Resources);
        Assert.Equal(2, appHost.Resources.Count);
 
        var apiService = appHost.Resources.First(r => r.Name == "apiservice");
        Assert.Equal("Project", apiService.ResourceType);
        Assert.Equal("Running", apiService.State);
        Assert.NotNull(apiService.Urls);
        Assert.Single(apiService.Urls);
        Assert.Equal("https://localhost:7001", apiService.Urls[0].Url);
 
        var redis = appHost.Resources.First(r => r.Name == "redis");
        Assert.Equal("Container", redis.ResourceType);
        Assert.Equal("Running", redis.State);
    }
 
    [Fact]
    public async Task PsCommand_WithoutResourcesOption_OmitsResourcesFromJsonOutput()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var textWriter = new TestOutputTextWriter(outputHelper);
 
        var monitor = new TestAuxiliaryBackchannelMonitor();
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            IsInScope = true,
            AppHostInfo = new AppHostInformation
            {
                AppHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "App1", "App1.AppHost.csproj"),
                ProcessId = 1234
            },
            ResourceSnapshots =
            [
                new ResourceSnapshot
                {
                    Name = "apiservice",
                    ResourceType = "Project",
                    State = "Running"
                }
            ]
        };
        monitor.AddConnection("hash1", "socket.hash1", connection);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.OutputTextWriter = textWriter;
            options.AuxiliaryBackchannelMonitorFactory = _ => monitor;
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        var result = command.Parse("ps --format json");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(ExitCodeConstants.Success, exitCode);
 
        var jsonOutput = string.Join(string.Empty, textWriter.Logs);
        var appHosts = JsonSerializer.Deserialize(jsonOutput, PsCommandJsonContext.RelaxedEscaping.ListAppHostDisplayInfo);
        Assert.NotNull(appHosts);
        Assert.Single(appHosts);
        Assert.Null(appHosts[0].Resources);
 
        // Also verify the raw JSON doesn't contain a "resources" key
        var document = JsonDocument.Parse(jsonOutput);
        var firstElement = document.RootElement[0];
        Assert.False(firstElement.TryGetProperty("resources", out _));
    }
 
    [Fact]
    public async Task PsCommand_ResourcesOption_TableFormat_DoesNotFetchResources()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var textWriter = new TestOutputTextWriter(outputHelper);
 
        var resourcesFetched = false;
        var monitor = new TestAuxiliaryBackchannelMonitor();
        var connection = new TestAppHostAuxiliaryBackchannel
        {
            IsInScope = true,
            AppHostInfo = new AppHostInformation
            {
                AppHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "App1", "App1.AppHost.csproj"),
                ProcessId = 1234
            },
            GetResourceSnapshotsHandler = _ =>
            {
                resourcesFetched = true;
                return Task.FromResult(new List<ResourceSnapshot>
                {
                    new ResourceSnapshot { Name = "apiservice", ResourceType = "Project", State = "Running" }
                });
            }
        };
        monitor.AddConnection("hash1", "socket.hash1", connection);
 
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
        {
            options.OutputTextWriter = textWriter;
            options.AuxiliaryBackchannelMonitorFactory = _ => monitor;
        });
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<RootCommand>();
        // --resources with table format should not fetch resources
        var result = command.Parse("ps --resources");
 
        var exitCode = await result.InvokeAsync().DefaultTimeout();
 
        Assert.Equal(ExitCodeConstants.Success, exitCode);
        Assert.False(resourcesFetched, "Resources should not be fetched when output format is table");
    }
}