// 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,
});
}
}