// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#pragma warning disable ASPIREAZURE003 // Private networking APIs are experimental.
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Dotnet;
using Aspire.Hosting.Utils;
using static Aspire.Hosting.Utils.AzureManifestUtils;
namespace Aspire.Hosting.Azure.Tests;
public class AzureFrontDoorAppServicePrivateLinkTests(ITestOutputHelper testOutputHelper)
{
[Fact]
public async Task PublishExposesWebsiteOutputsAsInfrastructureInputs()
{
using var workspace = TemporaryWorkspace.Create(testOutputHelper);
using var builder = TestDistributedApplicationBuilder.Create(
DistributedApplicationOperation.Publish, testOutputHelper, workspace.Path);
var environment = builder.AddAzureAppServiceEnvironment("env").WithDashboard(false);
var frontDoor = builder.AddAzureFrontDoor("frontdoor");
environment.WithAzureFrontDoorPrivateLink(frontDoor);
var api = AddProject(builder, "api");
frontDoor.WithOrigin(api);
using var app = builder.Build();
await app.RunAsync();
await Verify(await File.ReadAllTextAsync(Path.Combine(workspace.Path, "main.bicep")), "bicep");
Assert.True(File.Exists(Path.Combine(workspace.Path, "api", "api.bicep")));
Assert.True(File.Exists(Path.Combine(workspace.Path, "frontdoor", "frontdoor.bicep")));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task PrivateLinkTargetsEachWebsite(bool connectBeforeOrigins)
{
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, testOutputHelper);
var environment = builder.AddAzureAppServiceEnvironment("env").WithDashboard(false);
var frontDoor = builder.AddAzureFrontDoor("frontdoor");
if (connectBeforeOrigins)
{
environment.WithAzureFrontDoorPrivateLink(frontDoor);
}
var api = AddProject(builder, "api");
var web = AddProject(builder, "web");
frontDoor.WithOrigin(api).WithOrigin(web);
if (!connectBeforeOrigins)
{
environment.WithAzureFrontDoorPrivateLink(frontDoor);
}
using var app = builder.Build();
await ExecuteBeforeStartHooksAsync(app, default);
var (_, frontDoorBicep) = await GetManifestWithBicep(frontDoor.Resource);
var (_, apiBicep) = await GetManifestWithBicep(GetWebsite(api.Resource, environment.Resource));
var (_, webBicep) = await GetManifestWithBicep(GetWebsite(web.Resource, environment.Resource));
await Verify(new { frontDoorBicep, apiBicep, webBicep })
.UseParameters(connectBeforeOrigins);
}
[Fact]
public async Task ExistingPlanCanHostPrivateWebsites()
{
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, testOutputHelper);
var environment = builder.AddAzureAppServiceEnvironment("env")
.PublishAsExisting("existing-plan", "existing-rg")
.WithDashboard(false);
var frontDoor = builder.AddAzureFrontDoor("frontdoor");
environment.WithAzureFrontDoorPrivateLink(frontDoor);
var api = AddProject(builder, "api");
frontDoor.WithOrigin(api);
using var app = builder.Build();
await ExecuteBeforeStartHooksAsync(app, default);
var (_, bicep) = await GetManifestWithBicep(GetWebsite(api.Resource, environment.Resource));
await Verify(bicep, "bicep");
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task DeploymentSlotsTargetProductionSiteAndStayPrivate(bool parameterized)
{
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, testOutputHelper);
var environment = builder.AddAzureAppServiceEnvironment("env").WithDashboard(false);
if (parameterized)
{
environment.WithDeploymentSlot(builder.AddParameter("slot"));
}
else
{
environment.WithDeploymentSlot("staging");
}
var frontDoor = builder.AddAzureFrontDoor("frontdoor");
environment.WithAzureFrontDoorPrivateLink(frontDoor);
var api = AddProject(builder, "api");
frontDoor.WithOrigin(api);
using var app = builder.Build();
await ExecuteBeforeStartHooksAsync(app, default);
var (_, bicep) = await GetManifestWithBicep(GetWebsite(api.Resource, environment.Resource));
await Verify(bicep, "bicep").UseParameters(parameterized);
}
[Fact]
public async Task MixedComputeEnvironmentsUseTheirOwnPrivateLinkTargets()
{
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, testOutputHelper);
var appService = builder.AddAzureAppServiceEnvironment("sites").WithDashboard(false);
var containerApps = builder.AddAzureContainerAppEnvironment("apps");
var publicEnvironment = builder.AddAzureAppServiceEnvironment("public-env").WithDashboard(false);
var frontDoor = builder.AddAzureFrontDoor("frontdoor");
appService.WithAzureFrontDoorPrivateLink(frontDoor);
containerApps.WithAzureFrontDoorPrivateLink(frontDoor);
var web = AddProject(builder, "web").WithComputeEnvironment(appService);
var api = AddProject(builder, "api").WithComputeEnvironment(containerApps);
var publicApp = AddProject(builder, "public-app").WithComputeEnvironment(publicEnvironment);
frontDoor.WithOrigin(web).WithOrigin(api).WithOrigin(publicApp);
using var app = builder.Build();
await ExecuteBeforeStartHooksAsync(app, default);
var (_, frontDoorBicep) = await GetManifestWithBicep(frontDoor.Resource);
var (_, publicWebsiteBicep) = await GetManifestWithBicep(GetWebsite(publicApp.Resource, publicEnvironment.Resource));
await Verify(new { frontDoorBicep, publicWebsiteBicep });
}
[Fact]
public async Task WebsitesWithoutOriginsAreAlsoPrivate()
{
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, testOutputHelper);
var environment = builder.AddAzureAppServiceEnvironment("env").WithDashboard(false);
var frontDoor = builder.AddAzureFrontDoor("frontdoor");
environment.WithAzureFrontDoorPrivateLink(frontDoor);
var api = AddProject(builder, "api");
var other = AddProject(builder, "other");
frontDoor.WithOrigin(api);
using var app = builder.Build();
await ExecuteBeforeStartHooksAsync(app, default);
var (_, bicep) = await GetManifestWithBicep(GetWebsite(other.Resource, environment.Resource));
await Verify(bicep, "bicep");
}
[Fact]
public async Task UnconnectedFrontDoorCannotReachPrivateWebsites()
{
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, testOutputHelper);
var environment = builder.AddAzureAppServiceEnvironment("env");
var connected = builder.AddAzureFrontDoor("connected");
var unconnected = builder.AddAzureFrontDoor("unconnected");
environment.WithAzureFrontDoorPrivateLink(connected);
var api = AddProject(builder, "api");
connected.WithOrigin(api);
unconnected.WithOrigin(api);
using var app = builder.Build();
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => ExecuteBeforeStartHooksAsync(app, default));
Assert.Contains("Origin 'api' of Azure Front Door 'unconnected'", exception.ToString());
}
[Fact]
public async Task UnusedConnectionIsRejected()
{
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, testOutputHelper);
var environment = builder.AddAzureAppServiceEnvironment("env");
var frontDoor = builder.AddAzureFrontDoor("frontdoor");
environment.WithAzureFrontDoorPrivateLink(frontDoor);
using var app = builder.Build();
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => ExecuteBeforeStartHooksAsync(app, default));
Assert.Contains("none of its origins run in that environment", exception.ToString());
}
[Fact]
public void RunModeIsUnchanged()
{
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run, testOutputHelper);
var environment = builder.AddAzureAppServiceEnvironment("env").WithDeploymentSlot("staging");
var frontDoor = builder.AddAzureFrontDoor("frontdoor");
Assert.Same(environment, environment.WithAzureFrontDoorPrivateLink(frontDoor));
Assert.Empty(environment.Resource.Annotations.OfType<PrivateEndpointTargetAnnotation>());
Assert.Empty(frontDoor.Resource.Annotations.OfType<AzureFrontDoorPrivateLinkAnnotation>());
}
private static AzureAppServiceWebSiteResource GetWebsite(IResource resource, AzureAppServiceEnvironmentResource environment) =>
Assert.IsType<AzureAppServiceWebSiteResource>(resource.GetDeploymentTargetAnnotation(environment)?.DeploymentTarget);
private static IResourceBuilder<DotnetProjectResource> AddProject(IDistributedApplicationBuilder builder, string name) =>
builder.AddDotnetProject(name, "project.csproj", options => options.ExcludeLaunchProfile = true)
.WithHttpEndpoint(targetPort: 8080)
.WithExternalHttpEndpoints();
}