// 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 ASPIRECOMPUTE003 // Type is for evaluation purposes only and is subject to change or removal in future updates.
#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates.
#pragma warning disable ASPIREACANAMING001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure.AppContainers;
using System.Runtime.CompilerServices;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
namespace Aspire.Hosting.Azure.Tests;
public class AzureContainerAppEnvironmentExtensionsTests(ITestOutputHelper outputHelper)
{
[Fact]
public void AddAsExistingResource_ShouldBeIdempotent_ForAzureContainerAppEnvironmentResource()
{
// Arrange
var containerAppEnvironmentResource = new AzureContainerAppEnvironmentResource("test-container-app-env", _ => { });
var infrastructure = new AzureResourceInfrastructure(containerAppEnvironmentResource, "test-container-app-env");
// Act - Call AddAsExistingResource twice
var firstResult = containerAppEnvironmentResource.AddAsExistingResource(infrastructure);
var secondResult = containerAppEnvironmentResource.AddAsExistingResource(infrastructure);
// Assert - Both calls should return the same resource instance, not duplicates
Assert.Same(firstResult, secondResult);
}
[Fact]
public async Task AddAsExistingResource_RespectsExistingAzureResourceAnnotation_ForAzureContainerAppEnvironmentResource()
{
using var builder = TestDistributedApplicationBuilder.Create();
var existingName = builder.AddParameter("existing-env-name");
var existingResourceGroup = builder.AddParameter("existing-env-rg");
var containerAppEnvironment = builder.AddAzureContainerAppEnvironment("test-container-app-env")
.AsExisting(existingName, existingResourceGroup);
var module = builder.AddAzureInfrastructure("mymodule", infra =>
{
_ = containerAppEnvironment.Resource.AddAsExistingResource(infra);
});
var (manifest, bicep) = await AzureManifestUtils.GetManifestWithBicep(module.Resource, skipPreparer: true);
await Verify(manifest.ToString(), "json")
.AppendContentAsFile(bicep, "bicep");
}
[Fact]
public async Task WithAzureLogAnalyticsWorkspace_RespectsExistingWorkspaceInDifferentResourceGroup()
{
// Arrange
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
// Create parameters for existing Log Analytics Workspace in resource group "X"
var lawName = builder.AddParameter("log-env-shared-name");
var lawResourceGroup = builder.AddParameter("log-env-shared-rg"); // resource group "X"
// Create Log Analytics Workspace resource marked as existing in resource group "X"
var logAnalyticsWorkspace = builder
.AddAzureLogAnalyticsWorkspace("log-env-shared")
.AsExisting(lawName, lawResourceGroup);
// Create Container App Environment in resource group "Y" that references the existing LAW
var containerAppEnvironment = builder
.AddAzureContainerAppEnvironment("app-host") // This will be deployed to default resource group "Y"
.WithAzureLogAnalyticsWorkspace(logAnalyticsWorkspace);
// Verify that the LAW has the ExistingAzureResourceAnnotation
Assert.True(logAnalyticsWorkspace.Resource.TryGetLastAnnotation<ExistingAzureResourceAnnotation>(out var existingAnnotation));
Assert.Equal(lawName.Resource, existingAnnotation.Name);
Assert.Equal(lawResourceGroup.Resource, existingAnnotation.ResourceGroup);
// Verify that the Container App Environment has the AzureLogAnalyticsWorkspaceReferenceAnnotation
Assert.True(containerAppEnvironment.Resource.TryGetLastAnnotation<AzureLogAnalyticsWorkspaceReferenceAnnotation>(out var workspaceRef));
Assert.Same(logAnalyticsWorkspace.Resource, workspaceRef.Workspace);
// Act & Assert - Generate bicep and verify using snapshot testing
var (manifest, bicep) = await AzureManifestUtils.GetManifestWithBicep(containerAppEnvironment.Resource);
await Verify(bicep, extension: "bicep")
.AppendContentAsFile(manifest.ToString(), "json");
}
[Fact]
public void ContainerRegistry_ReturnsDefaultContainerRegistry()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var containerAppEnvironment = builder.AddAzureContainerAppEnvironment("env");
// The environment should have a default container registry set up
var registry = containerAppEnvironment.Resource.ContainerRegistry;
Assert.NotNull(registry);
Assert.IsType<AzureContainerRegistryResource>(registry);
}
[Fact]
public void ContainerRegistry_PrefersExplicitContainerRegistry()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var acr = builder.AddAzureContainerRegistry("myacr");
var containerAppEnvironment = builder.AddAzureContainerAppEnvironment("env")
.WithAzureContainerRegistry(acr);
// Should return the explicitly set registry
var registry = containerAppEnvironment.Resource.ContainerRegistry;
Assert.Same(acr.Resource, registry);
}
[Fact]
public void ContainerRegistry_ReturnsNullWhenNoRegistryConfigured()
{
// Create an environment resource without the builder to avoid automatic registry setup
var environment = new AzureContainerAppEnvironmentResource("env", _ => { });
Assert.Null(environment.ContainerRegistry);
}
[Fact]
public void ContainerRegistry_ThrowsWhenNonAzureRegistryConfigured()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var dockerRegistry = builder.AddContainerRegistry("docker-hub", "docker.io", "myuser");
var containerAppEnvironment = builder.AddAzureContainerAppEnvironment("env")
.WithContainerRegistry(dockerRegistry);
// Should throw because a non-Azure registry is configured
var exception = Assert.Throws<InvalidOperationException>(() => containerAppEnvironment.Resource.ContainerRegistry);
Assert.Contains("not an Azure Container Registry", exception.Message);
Assert.Contains("env", exception.Message);
}
[Fact]
public async Task WithDelegatedSubnet_ConfiguresVnetConfiguration()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var vnet = builder.AddAzureVirtualNetwork("myvnet");
var subnet = vnet.AddSubnet("container-apps-subnet", "10.0.0.0/23");
var containerAppEnvironment = builder.AddAzureContainerAppEnvironment("env")
.WithDelegatedSubnet(subnet);
var (_, envBicep) = await AzureManifestUtils.GetManifestWithBicep(containerAppEnvironment.Resource);
var (_, vnetBicep) = await AzureManifestUtils.GetManifestWithBicep(vnet.Resource);
await Verify(envBicep, extension: "bicep")
.AppendContentAsFile(vnetBicep, "bicep", "vnet");
}
[Fact]
public async Task AsExisting_PublishGeneratesThinModuleReferencingExistingEnvironment()
{
// Regression test for https://github.com/microsoft/aspire/issues/12977.
// When AsExisting is used on AddAzureContainerAppEnvironment, the published Bicep
// must reference the existing managed environment instead of generating a new one
// plus a new Log Analytics workspace and a new Aspire Dashboard component.
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var environmentName = builder.AddParameter("environmentName");
var sharedResourceGroup = builder.AddParameter("sharedRg");
var env = builder.AddAzureContainerAppEnvironment("env")
.AsExisting(environmentName, sharedResourceGroup);
var (manifest, bicep) = await AzureManifestUtils.GetManifestWithBicep(env.Resource);
await Verify(bicep, extension: "bicep")
.AppendContentAsFile(manifest.ToString(), "json");
}
[Fact]
public async Task AsExisting_WithExplicitContainerRegistry_PublishGeneratesThinModule()
{
// When AsExisting is combined with an explicit AsExisting ACR (a common deployment
// scenario where everything is pre-provisioned), the resulting Bicep should not
// create either a new managed environment or a new container registry.
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var environmentName = builder.AddParameter("environmentName");
var registryName = builder.AddParameter("registryName");
var sharedResourceGroup = builder.AddParameter("sharedRg");
var acr = builder.AddAzureContainerRegistry("acr")
.AsExisting(registryName, sharedResourceGroup);
var env = builder.AddAzureContainerAppEnvironment("env")
.AsExisting(environmentName, sharedResourceGroup)
.WithAzureContainerRegistry(acr);
var (manifest, bicep) = await AzureManifestUtils.GetManifestWithBicep(env.Resource);
await Verify(bicep, extension: "bicep")
.AppendContentAsFile(manifest.ToString(), "json");
}
[Fact]
public async Task WithAcrPullIdentity_PublishGeneratesEnvWithSuppliedIdentity()
{
// Greenfield environment, but the user has BYO'd a managed identity. The generated
// Bicep should NOT declare an env_mi resource or an AcrPull role assignment - the
// identity id should flow into the env module via a parameter and be emitted as
// AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID.
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var mi = builder.AddAzureUserAssignedIdentity("shared-mi");
var env = builder.AddAzureContainerAppEnvironment("env")
.WithAcrPullIdentity(mi);
var (manifest, bicep) = await AzureManifestUtils.GetManifestWithBicep(env.Resource);
await Verify(bicep, extension: "bicep")
.AppendContentAsFile(manifest.ToString(), "json");
}
[Fact]
public async Task WithAcrPullIdentity_AsExisting_PublishGeneratesThinModuleWithSuppliedIdentity()
{
// Full pre-provisioned scenario from https://github.com/microsoft/aspire/issues/12977:
// existing env + existing ACR + BYO identity that already has AcrPull on the ACR.
// The generated env module should reference existing resources only and contain
// neither a new identity nor a new role assignment.
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var environmentName = builder.AddParameter("environmentName");
var registryName = builder.AddParameter("registryName");
var identityName = builder.AddParameter("identityName");
var sharedResourceGroup = builder.AddParameter("sharedRg");
var acr = builder.AddAzureContainerRegistry("acr")
.AsExisting(registryName, sharedResourceGroup);
var mi = builder.AddAzureUserAssignedIdentity("shared-mi")
.AsExisting(identityName, sharedResourceGroup);
var env = builder.AddAzureContainerAppEnvironment("env")
.AsExisting(environmentName, sharedResourceGroup)
.WithAzureContainerRegistry(acr)
.WithAcrPullIdentity(mi);
var (manifest, bicep) = await AzureManifestUtils.GetManifestWithBicep(env.Resource);
await Verify(bicep, extension: "bicep")
.AppendContentAsFile(manifest.ToString(), "json");
}
[Fact]
public async Task CrossResourceGroupRegistry_UsesStandaloneAcrPullIdentityForFinalRegistry()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var firstRegistry = builder.AddAzureContainerRegistry("first-registry")
.PublishAsExisting("firstacr", "first-resource-group");
var provisionedRegistry = builder.AddAzureContainerRegistry("provisioned-registry");
var finalRegistry = builder.AddAzureContainerRegistry("final-registry")
.PublishAsExisting("myacr", "my-existing-resource-group");
var env = builder.AddAzureContainerAppEnvironment("env")
.WithAzureContainerRegistry(firstRegistry)
.WithAzureContainerRegistry(provisionedRegistry)
.WithAzureContainerRegistry(finalRegistry);
using var app = builder.Build();
await AzureManifestUtils.ExecuteBeforeStartHooksAsync(app, default);
await AzureManifestUtils.ExecuteBeforeStartHooksAsync(app, default);
var model = app.Services.GetRequiredService<DistributedApplicationModel>();
var identity = Assert.Single(model.Resources.OfType<AzureUserAssignedIdentityResource>(), resource => resource.Name == "env-mi");
var roles = Assert.Single(model.Resources.OfType<AzureRoleAssignmentResource>(), resource => resource.Name == "env-mi-roles-final-registry");
Assert.Same(finalRegistry.Resource, roles.TargetAzureResource);
var (_, envBicep) = await AzureManifestUtils.GetManifestWithBicep(env.Resource, skipPreparer: true);
var (_, identityBicep) = await AzureManifestUtils.GetManifestWithBicep(identity, skipPreparer: true);
var (rolesManifest, rolesBicep) = await AzureManifestUtils.GetManifestWithBicep(roles, skipPreparer: true);
await Verify(envBicep, extension: "bicep")
.AppendContentAsFile(identityBicep, "bicep", "identity")
.AppendContentAsFile(rolesBicep, "bicep", "roles")
.AppendContentAsFile(rolesManifest.ToString(), "json", "roles");
}
[Fact]
public async Task CrossSubscriptionRegistry_RoleModulePreservesSubscriptionAndResourceGroupScope()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var registry = builder.AddAzureContainerRegistry("registry")
.PublishAsExistingInResourceGroup("myacr", "my-existing-resource-group", "00000000-0000-0000-0000-000000000001");
builder.AddAzureContainerAppEnvironment("env")
.WithAzureContainerRegistry(registry);
using var app = builder.Build();
await AzureManifestUtils.ExecuteBeforeStartHooksAsync(app, default);
var model = app.Services.GetRequiredService<DistributedApplicationModel>();
var roles = Assert.Single(model.Resources.OfType<AzureRoleAssignmentResource>(), resource => resource.Name == "env-mi-roles-registry");
var (rolesManifest, _) = await AzureManifestUtils.GetManifestWithBicep(roles, skipPreparer: true);
await Verify(rolesManifest.ToString(), "json");
}
[Fact]
public async Task CrossResourceGroupRegistry_ThrowsTargetedErrorWhenGeneratedIdentityNameAlreadyExists()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var registry = builder.AddAzureContainerRegistry("registry")
.PublishAsExisting("myacr", "my-existing-resource-group");
builder.AddAzureUserAssignedIdentity("env-mi");
builder.AddAzureContainerAppEnvironment("env")
.WithAzureContainerRegistry(registry);
using var app = builder.Build();
var exception = await Assert.ThrowsAsync<DistributedApplicationException>(
() => AzureManifestUtils.ExecuteBeforeStartHooksAsync(app, default));
Assert.Equal(
"Cannot create the cross-scope ACR pull identity 'env-mi' for environment 'env' because a resource with that name already exists. Call 'WithAcrPullIdentity' on the environment to select an existing identity, or use a different resource name.",
exception.Message);
}
[Fact]
public async Task CrossResourceGroupRegistry_WithAzdNaming_PreservesIdentityName()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var registry = builder.AddAzureContainerRegistry("registry")
.PublishAsExisting("myacr", "my-existing-resource-group");
builder.AddAzureContainerAppEnvironment("env")
.WithAzureContainerRegistry(registry)
.WithAzdResourceNaming();
using var app = builder.Build();
await AzureManifestUtils.ExecuteBeforeStartHooksAsync(app, default);
var model = app.Services.GetRequiredService<DistributedApplicationModel>();
var identity = Assert.Single(model.Resources.OfType<AzureUserAssignedIdentityResource>(), resource => resource.Name == "env-mi");
var (_, identityBicep) = await AzureManifestUtils.GetManifestWithBicep(identity, skipPreparer: true);
await Verify(identityBicep, extension: "bicep");
}
[Fact]
public async Task CrossResourceGroupRegistry_WithCompactNaming_PreservesDefaultIdentityName()
{
static async Task<string> GetIdentityBicepAsync(bool useCompactNaming)
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var registry = builder.AddAzureContainerRegistry("registry")
.PublishAsExisting("myacr", "my-existing-resource-group");
var env = builder.AddAzureContainerAppEnvironment("env")
.WithAzureContainerRegistry(registry);
if (useCompactNaming)
{
env.WithCompactResourceNaming();
}
using var app = builder.Build();
await AzureManifestUtils.ExecuteBeforeStartHooksAsync(app, default);
var model = app.Services.GetRequiredService<DistributedApplicationModel>();
var identity = Assert.Single(model.Resources.OfType<AzureUserAssignedIdentityResource>(), resource => resource.Name == "env-mi");
var (_, identityBicep) = await AzureManifestUtils.GetManifestWithBicep(identity, skipPreparer: true);
return identityBicep;
}
Assert.Equal(
await GetIdentityBicepAsync(useCompactNaming: false),
await GetIdentityBicepAsync(useCompactNaming: true));
}
[Fact]
public async Task SameScopeExistingRegistry_KeepsInlineAcrPullIdentity()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var registry = builder.AddAzureContainerRegistry("registry")
.PublishAsExisting("myacr", resourceGroup: null);
builder.AddAzureContainerAppEnvironment("env")
.WithAzureContainerRegistry(registry);
using var app = builder.Build();
await AzureManifestUtils.ExecuteBeforeStartHooksAsync(app, default);
var model = app.Services.GetRequiredService<DistributedApplicationModel>();
Assert.Null(model.Resources.OfType<AzureUserAssignedIdentityResource>().SingleOrDefault(resource => resource.Name == "env-mi"));
Assert.Null(model.Resources.OfType<AzureRoleAssignmentResource>().SingleOrDefault(resource => resource.Name.StartsWith("env-mi-roles-", StringComparison.Ordinal)));
}
[Fact]
public async Task CrossResourceGroupRegistry_PreservesAcrPullIdentityAddedAfterEnvironment()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var registry = builder.AddAzureContainerRegistry("registry")
.PublishAsExisting("myacr", "my-existing-resource-group");
var env = builder.AddAzureContainerAppEnvironment("env")
.WithAzureContainerRegistry(registry);
var identity = builder.AddAzureUserAssignedIdentity("env-mi");
env.WithAcrPullIdentity(identity);
using var app = builder.Build();
await AzureManifestUtils.ExecuteBeforeStartHooksAsync(app, default);
var model = app.Services.GetRequiredService<DistributedApplicationModel>();
Assert.Same(identity.Resource, Assert.Single(model.Resources.OfType<AzureUserAssignedIdentityResource>(), resource => resource.Name == "env-mi"));
Assert.Null(model.Resources.OfType<AzureRoleAssignmentResource>().SingleOrDefault(resource => resource.Name.StartsWith("env-mi-roles-", StringComparison.Ordinal)));
}
[Fact]
public async Task MultipleEnvironments_SharingCrossScopeRegistry_EachGetDistinctStandaloneAcrPullIdentity()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
// A single existing registry in another resource group, shared by two environments in the same app host.
var registry = builder.AddAzureContainerRegistry("registry")
.PublishAsExisting("myacr", "my-existing-resource-group");
// Use letter-distinct environment names. The ACA managed environment name keeps only letters
// (digits are stripped), so names differing only by a digit (e.g. env1/env2) resolve to the same
// managed environment name within one resource group and trip ValidateManagedEnvironmentNames.
builder.AddAzureContainerAppEnvironment("east")
.WithAzureContainerRegistry(registry);
builder.AddAzureContainerAppEnvironment("west")
.WithAzureContainerRegistry(registry);
using var app = builder.Build();
await AzureManifestUtils.ExecuteBeforeStartHooksAsync(app, default);
await AzureManifestUtils.ExecuteBeforeStartHooksAsync(app, default);
var model = app.Services.GetRequiredService<DistributedApplicationModel>();
// Each environment promotes its own standalone identity, disambiguated by the environment's (model-unique) name.
var identityEast = Assert.Single(model.Resources.OfType<AzureUserAssignedIdentityResource>(), resource => resource.Name == "east-mi");
var identityWest = Assert.Single(model.Resources.OfType<AzureUserAssignedIdentityResource>(), resource => resource.Name == "west-mi");
Assert.NotSame(identityEast, identityWest);
// Each environment gets its own AcrPull role module scoped to the shared registry, so the two grants never collide.
var rolesEast = Assert.Single(model.Resources.OfType<AzureRoleAssignmentResource>(), resource => resource.Name == "east-mi-roles-registry");
var rolesWest = Assert.Single(model.Resources.OfType<AzureRoleAssignmentResource>(), resource => resource.Name == "west-mi-roles-registry");
Assert.Same(registry.Resource, rolesEast.TargetAzureResource);
Assert.Same(registry.Resource, rolesWest.TargetAzureResource);
}
[Fact]
public async Task AsExisting_ThrowsWhenCombinedWithDelegatedSubnet()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var environmentName = builder.AddParameter("environmentName");
var vnet = builder.AddAzureVirtualNetwork("myvnet");
var subnet = vnet.AddSubnet("aca-subnet", "10.0.0.0/23");
var env = builder.AddAzureContainerAppEnvironment("env")
.AsExisting(environmentName, (IResourceBuilder<ParameterResource>?)null)
.WithDelegatedSubnet(subnet);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => AzureManifestUtils.GetManifestWithBicep(env.Resource));
Assert.Contains("existing", ex.Message, StringComparison.OrdinalIgnoreCase);
Assert.Contains("WithDelegatedSubnet", ex.Message);
}
[Fact]
public async Task AsExisting_ThrowsWhenContainerAppDeclaresVolumeMount()
{
// When the env is marked as existing, Aspire cannot provision the storage
// account / file share that backs container app volume mounts (storage is
// attached to the managed env, which we don't own here). Asserts the
// configureInfrastructure callback throws a clear error in that case so the
// guard isn't silently dropped by future refactors.
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var environmentName = builder.AddParameter("environmentName");
var env = builder.AddAzureContainerAppEnvironment("env")
.AsExisting(environmentName, (IResourceBuilder<ParameterResource>?)null);
builder.AddContainer("api", "myimage")
.WithVolume("data", "/var/data");
using var app = builder.Build();
await AzureManifestUtils.ExecuteBeforeStartHooksAsync(app, default);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => AzureManifestUtils.GetManifestWithBicep(env.Resource, skipPreparer: true));
Assert.Equal(
"The Azure Container App Environment 'env' is marked as existing but one or more " +
"container apps targeted to it declare volume mounts. Volumes require provisioning storage on the " +
"managed environment, which Aspire cannot do for an existing environment. Remove the volume mounts " +
"or stop marking the environment as existing.",
ex.Message);
}
[Fact]
public async Task AsExisting_ThrowsWhenCombinedWithAzureLogAnalyticsWorkspace()
{
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var environmentName = builder.AddParameter("environmentName");
var logAnalyticsWorkspace = builder.AddAzureLogAnalyticsWorkspace("log");
var env = builder.AddAzureContainerAppEnvironment("env")
.AsExisting(environmentName, (IResourceBuilder<ParameterResource>?)null)
.WithAzureLogAnalyticsWorkspace(logAnalyticsWorkspace);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => AzureManifestUtils.GetManifestWithBicep(env.Resource));
Assert.Equal(
"The Azure Container App Environment 'env' is marked as existing but is also " +
"configured with a Log Analytics workspace via WithAzureLogAnalyticsWorkspace. The existing managed " +
"environment already owns its Log Analytics workspace and Aspire cannot reconfigure it. Remove the " +
"WithAzureLogAnalyticsWorkspace call or stop marking the environment as existing.",
ex.Message);
}
[Fact]
public async Task WithAzureContainerRegistry_PublishSucceeds_WhenDefaultRegistryIsRedundant()
{
// Regression test for the publish-time error:
// "Step 'push-prereq' depends on unknown step 'login-to-acr-env-acr'"
// which occurred when an explicit container registry was supplied to an
// AzureContainerAppEnvironment. The env's prepare step removes the now-unused
// default '{env}-acr' registry from the model during BeforeStart. Without
// isolating the BeforeStart pipeline-resolve from the singleton pipeline,
// that resolve would have appended the default registry's login step name
// to the built-in 'push-prereq' step's DependsOnSteps list, leaving a stale
// dependency edge that caused the publish-time ResolveStepsAsync to fail.
using var workspace = TemporaryWorkspace.Create(outputHelper);
var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, workspace.Path);
var acr = builder.AddAzureContainerRegistry("acr");
builder.AddAzureContainerAppEnvironment("env")
.WithAzureContainerRegistry(acr);
builder.AddContainer("api", "myimage");
using var app = builder.Build();
// Publishing will stop the app when it is done.
await app.RunAsync();
var mainBicepPath = Path.Combine(workspace.Path, "main.bicep");
Assert.True(File.Exists(mainBicepPath), $"Expected publish to produce '{mainBicepPath}'.");
var mainBicep = await File.ReadAllTextAsync(mainBicepPath);
var envBicepPath = Path.Combine(workspace.Path, "env", "env.bicep");
Assert.True(File.Exists(envBicepPath), $"Expected publish to produce '{envBicepPath}'.");
var envBicep = await File.ReadAllTextAsync(envBicepPath);
await Verify(mainBicep, "bicep")
.AppendContentAsFile(envBicep, "bicep");
}
[Fact]
public async Task RunningTheDeploymentTargetHooksTwiceLeavesOneTargetPerEnvironment()
{
// The prepare-azure-container-apps step declares RequiredBySteps = [BeforeStart], so it runs
// once in the BeforeStart pipeline and again in the publish DAG. Without a guard the second
// pass adds a second DeploymentTargetAnnotation, and GetDeploymentTargetAnnotation then throws
// on the ambiguity, which fails every publish that goes through both paths.
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
var environment = builder.AddAzureContainerAppEnvironment("env");
builder.AddContainer("api", "nginx")
.WithHttpEndpoint(port: 8080, targetPort: 80, name: "http")
.WithComputeEnvironment(environment);
using var app = builder.Build();
await ExecuteBeforeStartHooksAsync(app, default);
await ExecuteBeforeStartHooksAsync(app, default);
var model = app.Services.GetRequiredService<DistributedApplicationModel>();
var api = model.Resources.First(r => r.Name == "api");
Assert.Single(api.Annotations.OfType<DeploymentTargetAnnotation>());
Assert.Same(environment.Resource, api.GetDeploymentTargetAnnotation()!.ComputeEnvironment);
}
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")]
private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken);
}