File: Commands\ConfigCommandTests.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.Configuration;
using Aspire.Cli.Tests.Utils;
using Microsoft.Extensions.DependencyInjection;
using System.Text.Json.Nodes;
using Xunit;
 
namespace Aspire.Cli.Tests.Commands;
 
public class ConfigCommandTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public async Task ConfigCommandReturnsInvalidCommandExitCode()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
        var result = command.Parse("config");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(ExitCodeConstants.InvalidCommand, exitCode);
    }
 
    [Fact]
    public async Task ConfigSetCommand_WithFlatKey_CreatesSimpleProperty()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
        var result = command.Parse("config set foo bar");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, exitCode);
 
        // Verify the settings file was created correctly
        var settingsPath = Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "settings.json");
        Assert.True(File.Exists(settingsPath));
 
        var json = await File.ReadAllTextAsync(settingsPath);
        var settings = JsonNode.Parse(json)?.AsObject();
        Assert.NotNull(settings);
        Assert.Equal("bar", settings["foo"]?.ToString());
    }
 
    [Fact]
    public async Task ConfigSetCommand_WithDotNotation_CreatesNestedObject()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
        var result = command.Parse("config set foo.bar baz");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, exitCode);
 
        // Verify the settings file was created correctly
        var settingsPath = Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "settings.json");
        Assert.True(File.Exists(settingsPath));
 
        var json = await File.ReadAllTextAsync(settingsPath);
        var settings = JsonNode.Parse(json)?.AsObject();
        Assert.NotNull(settings);
        Assert.True(settings["foo"] is JsonObject);
        var fooObject = settings["foo"]!.AsObject();
        Assert.Equal("baz", fooObject["bar"]?.ToString());
    }
 
    [Fact]
    public async Task ConfigSetCommand_WithDeepDotNotation_CreatesDeeplyNestedObject()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
        var result = command.Parse("config set foo.bar.baz hello");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, exitCode);
 
        // Verify the settings file was created correctly
        var settingsPath = Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "settings.json");
        Assert.True(File.Exists(settingsPath));
 
        var json = await File.ReadAllTextAsync(settingsPath);
        var settings = JsonNode.Parse(json)?.AsObject();
        Assert.NotNull(settings);
        
        Assert.True(settings["foo"] is JsonObject);
        var fooObject = settings["foo"]!.AsObject();
        Assert.True(fooObject["bar"] is JsonObject);
        var barObject = fooObject["bar"]!.AsObject();
        Assert.Equal("hello", barObject["baz"]?.ToString());
    }
 
    [Fact]
    public async Task ConfigSetCommand_ReplacesPrimitiveWithObject()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
        
        // First set a primitive value
        var result1 = command.Parse("config set foo primitive");
        var exitCode1 = await result1.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, exitCode1);
 
        // Then set a nested value that should replace the primitive
        var result2 = command.Parse("config set foo.bar nested");
        var exitCode2 = await result2.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, exitCode2);
 
        // Verify the primitive was replaced with an object
        var settingsPath = Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "settings.json");
        var json = await File.ReadAllTextAsync(settingsPath);
        var settings = JsonNode.Parse(json)?.AsObject();
        Assert.NotNull(settings);
        
        Assert.True(settings["foo"] is JsonObject);
        var fooObject = settings["foo"]!.AsObject();
        Assert.Equal("nested", fooObject["bar"]?.ToString());
    }
 
    [Fact]
    public async Task ConfigGetCommand_WithFlatKey_ReturnsValue()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services1 = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider1 = services1.BuildServiceProvider();
 
        var command1 = provider1.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
 
        // First set a value
        var setResult = command1.Parse("config set testkey testvalue");
        var setExitCode = await setResult.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, setExitCode);
 
        // Create a new service collection to reload config.
        var services2 = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider2 = services2.BuildServiceProvider();
 
        var command2 = provider2.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
 
        // Then get the value
        var getResult = command2.Parse("config get testkey");
        var getExitCode = await getResult.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, getExitCode);
    }
 
    [Fact]
    public async Task ConfigGetCommand_WithDotNotation_ReturnsNestedValue()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services1 = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider1 = services1.BuildServiceProvider();
 
        var command1 = provider1.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
 
        // First set a nested value
        var setResult = command1.Parse("config set level1.level2.level3 nestedvalue");
        var setExitCode = await setResult.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, setExitCode);
 
        var services2 = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider2 = services2.BuildServiceProvider();
 
        var command2 = provider2.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
 
        // Then get the nested value
        var getResult = command2.Parse("config get level1.level2.level3");
        var getExitCode = await getResult.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, getExitCode);
    }
 
    [Fact]
    public async Task ConfigGetCommand_WithNonExistentKey_ReturnsError()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
        var result = command.Parse("config get nonexistent.key");
 
        var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(10, exitCode);
    }
 
    [Fact]
    public async Task ConfigDeleteCommand_WithFlatKey_RemovesValue()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
 
        // First set a value
        var setResult = command.Parse("config set deletekey deletevalue");
        var setExitCode = await setResult.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, setExitCode);
 
        // Then delete the value
        var deleteResult = command.Parse("config delete deletekey");
        var deleteExitCode = await deleteResult.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, deleteExitCode);
 
        // Verify it's deleted
        var getResult = command.Parse("config get deletekey");
        var getExitCode = await getResult.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(10, getExitCode); // Should return error for missing key
    }
 
    [Fact]
    public async Task ConfigDeleteCommand_CleansUpEmptyParentObjects()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
 
        // Set a deeply nested value
        var setResult = command.Parse("config set deep.nested.value test");
        var setExitCode = await setResult.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, setExitCode);
 
        // Delete the nested value
        var deleteResult = command.Parse("config delete deep.nested.value");
        var deleteExitCode = await deleteResult.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, deleteExitCode);
 
        // Verify the entire deep.nested structure is cleaned up
        var settingsPath = Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "settings.json");
        var json = await File.ReadAllTextAsync(settingsPath);
        var settings = JsonNode.Parse(json)?.AsObject();
        Assert.NotNull(settings);
        
        // The deep object should be completely removed since it became empty
        Assert.False(settings.ContainsKey("deep"));
    }
 
    [Fact]
    public async Task ConfigListCommand_ShowsFlattenedDotNotationKeys()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider = services.BuildServiceProvider();
 
        var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
 
        // Set various values
        var setResult1 = command.Parse("config set flatkey flatvalue");
        var setExitCode1 = await setResult1.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, setExitCode1);
 
        var setResult2 = command.Parse("config set nested.key nestedvalue");
        var setExitCode2 = await setResult2.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, setExitCode2);
 
        var setResult3 = command.Parse("config set deep.nested.key deepvalue");
        var setExitCode3 = await setResult3.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, setExitCode3);
 
        // List all configuration
        var listResult = command.Parse("config list");
        var listExitCode = await listResult.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, listExitCode);
    }
 
    [Fact]
    public async Task FeatureFlags_WhenSetToTrue_ReturnsTrue()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(
            workspace,
            outputHelper,
            options => options.EnabledFeatures = new[] { KnownFeatures.DeployCommandEnabled }
            );
        var provider = services.BuildServiceProvider();
 
        // Set the feature flag to true
        var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
        var setResult = command.Parse($"config set {KnownFeatures.FeaturePrefix}.{KnownFeatures.DeployCommandEnabled} true");
        var setExitCode = await setResult.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, setExitCode);
 
        // Check the feature flag
        var featureFlags = provider.GetRequiredService<IFeatures>();
        Assert.True(featureFlags.IsFeatureEnabled(KnownFeatures.DeployCommandEnabled, defaultValue: false));
    }
 
    [Fact]
    public async Task FeatureFlags_WhenSetToFalse_ReturnsFalse()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => options.DisabledFeatures = new[] { KnownFeatures.DeployCommandEnabled });
        var provider = services.BuildServiceProvider();
 
        // Set the feature flag to false
        var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
        var setResult = command.Parse($"config set {KnownFeatures.FeaturePrefix}.{KnownFeatures.DeployCommandEnabled} false");
        var setExitCode = await setResult.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, setExitCode);
 
        // Check the feature flag
        var featureFlags = provider.GetRequiredService<IFeatures>();
        Assert.False(featureFlags.IsFeatureEnabled(KnownFeatures.DeployCommandEnabled, defaultValue: true));
    }
 
    [Fact]
    public async Task FeatureFlags_WhenSetToInvalidValue_ReturnsFalse()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(
            workspace,
            outputHelper,
            options => options.ConfigurationCallback += confing =>
            {
                confing[$"{KnownFeatures.FeaturePrefix}:{KnownFeatures.DeployCommandEnabled}"] = "invalid"; // Set an invalid value
            });
        var provider = services.BuildServiceProvider();
 
        // Set the feature flag to an invalid value
        var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
        var setResult = command.Parse($"config set {KnownFeatures.FeaturePrefix}.{KnownFeatures.DeployCommandEnabled} invalid");
        var setExitCode = await setResult.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, setExitCode);
 
        // Check the feature flag
        var featureFlags = provider.GetRequiredService<IFeatures>();
        Assert.False(featureFlags.IsFeatureEnabled(KnownFeatures.DeployCommandEnabled, defaultValue: true));
    }
 
    [Fact]
    public void DeployCommand_WhenFeatureFlagDisabled_IsNotAvailable()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider = services.BuildServiceProvider();
 
        var rootCommand = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
        
        // Check that deploy command is not available
        var hasDeployCommand = rootCommand.Subcommands.Any(cmd => cmd.Name == "deploy");
        Assert.False(hasDeployCommand);
    }
 
    [Fact]
    public async Task DeployCommand_WhenFeatureFlagEnabled_IsAvailable()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var provider = services.BuildServiceProvider();
 
        // Set the feature flag to true
        var command = provider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
        var setResult = command.Parse($"config set {KnownFeatures.FeaturePrefix}.{KnownFeatures.DeployCommandEnabled} true");
        var setExitCode = await setResult.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
        Assert.Equal(0, setExitCode);
 
        // Create a new service provider to get the updated configuration
        var newServices = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
        var newProvider = newServices.BuildServiceProvider();
        var newRootCommand = newProvider.GetRequiredService<Aspire.Cli.Commands.RootCommand>();
        
        // Check that deploy command is available
        var hasDeployCommand = newRootCommand.Subcommands.Any(cmd => cmd.Name == "deploy");
        Assert.True(hasDeployCommand);
    }
}