File: Dcp\DcpCliArgsTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.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.Hosting.Dcp;
using Aspire.Shared;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
 
namespace Aspire.Hosting.Tests.Dcp;
 
[Trait("Partition", "4")]
public class DcpCliArgsTests
{
    [Fact]
    public void TestDcpCliPathArgumentPopulatesConfig()
    {
        var builder = DistributedApplication.CreateBuilder([
            "--dcp-cli-path", "/not/a/valid/path",
            ]);
 
        Assert.Equal("/not/a/valid/path", builder.Configuration["DcpPublisher:CliPath"]);
    }
 
    [Fact]
    public void TestDcpDependencyCheckTimeoutPopulatesConfig()
    {
        var builder = DistributedApplication.CreateBuilder([
            "--dcp-dependency-check-timeout", "42",
            ]);
 
        Assert.Equal("42", builder.Configuration["DcpPublisher:DependencyCheckTimeout"]);
    }
 
    [Fact]
    public void TestDcpContainerRuntimePopulatesConfig()
    {
        var builder = DistributedApplication.CreateBuilder([
            "--dcp-container-runtime", "not-a-valid-container-runtime",
            ]);
 
        Assert.Equal("not-a-valid-container-runtime", builder.Configuration["DcpPublisher:ContainerRuntime"]);
    }
 
    [Fact]
    public void TestDcpOptionsPopulated()
    {
        var builder = DistributedApplication.CreateBuilder(
            [
            "--dcp-cli-path", "/not/a/valid/path",
            "--dcp-container-runtime", "not-a-valid-container-runtime",
            "--dcp-dependency-check-timeout", "42",
            "--dcp-dashboard-path", "/not/a/valid/path"
            ]);
 
        using var app = builder.Build();
        var dcpOptions = app.Services.GetRequiredService<IOptions<DcpOptions>>().Value;
 
        Assert.Equal("not-a-valid-container-runtime", dcpOptions.ContainerRuntime);
        Assert.Equal(42, dcpOptions.DependencyCheckTimeout);
        Assert.Equal("/not/a/valid/path", dcpOptions.CliPath);
        Assert.Equal("/not/a/valid/path", dcpOptions.DashboardPath);
    }
 
    [Fact]
    public void ExplicitDcpPublisherConfigurationOverridesBundlePaths()
    {
        var builder = DistributedApplication.CreateBuilder();
        var explicitDcpPath = Path.Combine("explicit", "dcp");
        var explicitDashboardPath = Path.Combine("explicit", "dashboard");
 
        builder.Configuration[BundleDiscovery.DcpPathEnvVar] = Path.Combine("bundle", "dcp");
        builder.Configuration[BundleDiscovery.DashboardPathEnvVar] = Path.Combine("bundle", "dashboard");
        AddDcpPublisherPathConfigurationOverride(builder.Configuration, explicitDcpPath, explicitDashboardPath);
 
        using var app = builder.Build();
        var dcpOptions = app.Services.GetRequiredService<IOptions<DcpOptions>>().Value;
 
        Assert.Equal(explicitDcpPath, dcpOptions.CliPath);
        Assert.Equal(Path.Combine("explicit", "ext"), dcpOptions.ExtensionsPath);
        Assert.Equal(explicitDashboardPath, dcpOptions.DashboardPath);
    }
 
    [Fact]
    public void BundlePathsPopulateDcpOptionsWhenExplicitDcpPublisherConfigurationIsNotSet()
    {
        var builder = DistributedApplication.CreateBuilder();
        var bundleDcpPath = Path.Combine("bundle", "dcp");
        var bundleDashboardPath = Path.Combine("bundle", "dashboard");
 
        builder.Configuration[BundleDiscovery.DcpPathEnvVar] = bundleDcpPath;
        builder.Configuration[BundleDiscovery.DashboardPathEnvVar] = bundleDashboardPath;
        AddDcpPublisherPathConfigurationOverride(builder.Configuration, string.Empty, string.Empty);
 
        using var app = builder.Build();
        var dcpOptions = app.Services.GetRequiredService<IOptions<DcpOptions>>().Value;
 
        Assert.Equal(BundleDiscovery.GetDcpExecutablePath(bundleDcpPath), dcpOptions.CliPath);
        Assert.Equal(Path.Combine(bundleDcpPath, "ext"), dcpOptions.ExtensionsPath);
        Assert.Equal(bundleDashboardPath, dcpOptions.DashboardPath);
    }
 
    [Fact]
    public void WhitespaceDcpPublisherPathConfigurationFallsBackToBundlePaths()
    {
        var builder = DistributedApplication.CreateBuilder();
        var bundleDcpPath = Path.Combine("bundle", "dcp");
        var bundleDashboardPath = Path.Combine("bundle", "dashboard");
 
        builder.Configuration[BundleDiscovery.DcpPathEnvVar] = bundleDcpPath;
        builder.Configuration[BundleDiscovery.DashboardPathEnvVar] = bundleDashboardPath;
        AddDcpPublisherPathConfigurationOverride(builder.Configuration, " ", "\t");
 
        using var app = builder.Build();
        var dcpOptions = app.Services.GetRequiredService<IOptions<DcpOptions>>().Value;
 
        Assert.Equal(BundleDiscovery.GetDcpExecutablePath(bundleDcpPath), dcpOptions.CliPath);
        Assert.Equal(Path.Combine(bundleDcpPath, "ext"), dcpOptions.ExtensionsPath);
        Assert.Equal(bundleDashboardPath, dcpOptions.DashboardPath);
    }
 
    [Theory]
    [InlineData(DistributedApplicationOperation.Run, true)]
    [InlineData(DistributedApplicationOperation.Publish, false)]
    public void DcpOptionsValidationRequiresPathsOnlyInRunMode(DistributedApplicationOperation operation, bool shouldFail)
    {
        var validator = new ValidateDcpOptions(new DistributedApplicationExecutionContext(operation));
        var result = validator.Validate(null, new DcpOptions
        {
            CliPath = " ",
            DashboardPath = "\t",
        });
 
        Assert.Equal(shouldFail, result.Failed);
        if (shouldFail)
        {
            Assert.Contains("The path to the DCP executable used for Aspire orchestration is required.", result.FailureMessage);
            Assert.Contains("The path to the Aspire Dashboard binaries is missing.", result.FailureMessage);
        }
    }
 
    [Fact]
    public void DcpOptionsValidationStillFailsForInvalidProxylessEndpointPortRangeInPublishMode()
    {
        var validator = new ValidateDcpOptions(new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish));
        var result = validator.Validate(null, new DcpOptions
        {
            ProxylessEndpointPortRangeStart = 32767,
            ProxylessEndpointPortRangeEnd = 10000,
        });
 
        Assert.True(result.Failed);
        Assert.Contains(
            "The proxyless endpoint port range start must be less than or equal to the range end.",
            Assert.Single(result.Failures!));
    }
 
    [Fact]
    public void KnownConfigProxylessEndpointPortRangeOverridesDcpPublisherConfiguration()
    {
        var builder = DistributedApplication.CreateBuilder();
        builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
        {
            ["DcpPublisher:ProxylessEndpointPortRangeStart"] = "10000",
            ["DcpPublisher:ProxylessEndpointPortRangeEnd"] = "10001",
            [KnownConfigNames.ProxylessEndpointPortRange] = "20000-20001",
        });
 
        using var app = builder.Build();
        var dcpOptions = app.Services.GetRequiredService<IOptions<DcpOptions>>().Value;
 
        Assert.Equal(20000, dcpOptions.ProxylessEndpointPortRangeStart);
        Assert.Equal(20001, dcpOptions.ProxylessEndpointPortRangeEnd);
    }
 
    [Fact]
    public void KnownConfigProxylessEndpointPortRangeRequiresStartEndFormat()
    {
        var builder = DistributedApplication.CreateBuilder();
        builder.Configuration[KnownConfigNames.ProxylessEndpointPortRange] = "20000";
        using var app = builder.Build();
 
        var exception = Assert.Throws<InvalidOperationException>(() => app.Services.GetRequiredService<IOptions<DcpOptions>>().Value);
 
        Assert.Contains(KnownConfigNames.ProxylessEndpointPortRange, exception.Message);
        Assert.Contains("start-end", exception.Message);
    }
 
    private static void AddDcpPublisherPathConfigurationOverride(ConfigurationManager configuration, string cliPath, string dashboardPath)
    {
        configuration.AddInMemoryCollection(new Dictionary<string, string?>
        {
            ["DcpPublisher:CliPath"] = cliPath,
            ["DcpPublisher:DashboardPath"] = dashboardPath,
        });
    }
}