Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Aspire.Hosting.Testing/DistributedApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ private static void PreConfigureBuilderOptions(
SetDefault(KnownConfigNames.AspNetCoreUrls, "http://localhost:8080");
SetDefaultFallback(KnownConfigNames.DashboardOtlpGrpcEndpointUrl, KnownConfigNames.Legacy.DashboardOtlpGrpcEndpointUrl, "http://localhost:4317");

// Since the testing builder defaults all dashboard/OTLP URLs to HTTP, also allow
// unsecured transport by default. This prevents OptionsValidationException when the
// user enables the dashboard (DisableDashboard = false) without explicitly setting
// ASPIRE_ALLOW_UNSECURED_TRANSPORT. See https://github.com/microsoft/aspire/issues/17622
SetDefault(KnownConfigNames.AllowUnsecuredTransport, "true");

var appHostProjectPath = ResolveProjectPath(entryPointAssembly);
if (!string.IsNullOrEmpty(appHostProjectPath) && Directory.Exists(appHostProjectPath))
{
Expand Down
20 changes: 9 additions & 11 deletions src/Aspire.Hosting/Dashboard/DashboardServiceHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,15 @@ void ConfigureKestrel(KestrelServerOptions kestrelOptions)
kestrelOptions.Listen(IPAddress.Loopback, port: 0, ConfigureListen);
_logger.LogDebug("Resource service endpoint not configured. Listening on {Scheme}://127.0.0.1:<random>.", scheme);
}
else if (IsLocalResourceServiceEndpoint(uri))
else if (IPAddress.TryParse(uri.Host, out var ip) && IPAddress.IsLoopback(ip))
{
// Listen on the requested localhost port.
// Bind to the exact loopback address specified (e.g. 127.0.0.1 or [::1]).
kestrelOptions.Listen(ip, uri.Port, ConfigureListen);
_logger.LogDebug("Resource service endpoint configured: {Uri}", uri);
}
else if (uri.IsLoopback || IsLocalhostOrLocalhostTld(uri))
{
// For "localhost" or *.localhost hosts, bind to both IPv4 and IPv6 loopback.
kestrelOptions.ListenLocalhost(uri.Port, ConfigureListen);
_logger.LogDebug("Resource service endpoint configured: {Uri}", uri);
}
Expand Down Expand Up @@ -178,16 +184,8 @@ internal static string ResolveScheme(Uri? configuredUri, bool allowUnsecuredTran
return allowUnsecuredTransport ? "http" : "https";
}

/// <summary>
/// Determines whether the resource service endpoint is scoped to the local machine.
/// </summary>
internal static bool IsLocalResourceServiceEndpoint(Uri uri)
private static bool IsLocalhostOrLocalhostTld(Uri uri)
{
if (uri.IsLoopback)
{
return true;
}

var host = uri.Host.EndsWith(".", StringComparison.Ordinal)
? uri.Host[..^1]
: uri.Host;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<Compile Include="$(TestsSharedDir)Logging\*.cs" LinkBase="shared\Logging" />
<Compile Include="$(TestsSharedDir)ConsoleLogging\*.cs" LinkBase="shared" />
<Compile Include="$(TestsSharedDir)DistributedApplicationTestingBuilderExtensions.cs" Link="shared/DistributedApplicationTestingBuilderExtensions.cs" />
<Compile Include="$(TestsSharedDir)AsyncTestHelpers.cs" Link="shared/AsyncTestHelpers.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
64 changes: 64 additions & 0 deletions tests/Aspire.Hosting.Testing.Tests/TestingBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Aspire.Hosting.Utils;
using Aspire.TestProject;
using Aspire.TestUtilities;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
Expand Down Expand Up @@ -568,6 +569,69 @@ public async Task StartAsyncAbandonedAfterHang()
}
}

[Fact]
[RequiresFeature(TestFeature.Docker)]
public async Task DashboardEnabledInTestingBuilderShouldWorkWithDynamicPorts()
{
var builder = DistributedApplicationTestingBuilder.Create([], (options, _) =>
{
options.DisableDashboard = false;
});
builder.WithTestAndResourceLogging(output);

await using var app = await builder.BuildAsync();

await app.StartAsync().DefaultTimeout(TestConstants.LongTimeoutTimeSpan);

// Wait for the dashboard to become healthy, confirming it is actually running.
await app.ResourceNotifications.WaitForResourceHealthyAsync(
"aspire-dashboard",
CancellationToken.None).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
}

[Theory]
[RequiresFeature(TestFeature.Docker)]
[InlineData("https://127.0.0.1:0")]
[InlineData("https://[::1]:0")]
public async Task LoopbackWithDynamicPorts(string endpointUrl)
{
var builder = DistributedApplicationTestingBuilder.Create([], (opt, _) =>
{
opt.DisableDashboard = false;
});
builder.WithTestAndResourceLogging(output);

builder.Configuration["ASPNETCORE_URLS"] = endpointUrl;
builder.Configuration["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = endpointUrl;
builder.Configuration["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = endpointUrl;

await using var app = await builder.BuildAsync();
await app.StartAsync().DefaultTimeout(TestConstants.LongTimeoutTimeSpan);

// Wait for the dashboard to become healthy, confirming it is actually running.
await app.ResourceNotifications.WaitForResourceHealthyAsync(
"aspire-dashboard",
CancellationToken.None).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
}

[Fact]
[RequiresFeature(TestFeature.Docker)]
public async Task NonLocalResourceServiceEndpointThrows()
{
var builder = DistributedApplicationTestingBuilder.Create([], (opt, _) =>
{
opt.DisableDashboard = false;
});
builder.WithTestAndResourceLogging(output);

builder.Configuration["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = "https://example.com:5001";

await using var app = await builder.BuildAsync();

var ex = await Assert.ThrowsAsync<ArgumentException>(() => app.StartAsync());
Assert.Equal("ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL must contain a local loopback address.", ex.Message);
}

private sealed record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,4 @@ public void ResolveScheme_ReturnsExpectedScheme(string? uriString, bool allowUns

Assert.Equal(expectedScheme, scheme);
}

[Theory]
[InlineData("https://localhost:5001", true)]
[InlineData("https://localhost.:5001", true)]
[InlineData("https://127.0.0.1:5001", true)]
[InlineData("https://[::1]:5001", true)]
[InlineData("https://myapp.dev.localhost:5001", true)]
[InlineData("https://myapp.dev.localhost.:5001", true)]
[InlineData("https://example.com:5001", false)]
[InlineData("https://localhost.example.com:5001", false)]
[InlineData("https://example-localhost:5001", false)]
public void IsLocalResourceServiceEndpoint_ReturnsExpectedResult(string uriString, bool expectedResult)
{
var result = DashboardServiceHost.IsLocalResourceServiceEndpoint(new Uri(uriString));

Assert.Equal(expectedResult, result);
}
}
Loading