File: AzureContainerAppEnvironmentExtensionsTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Azure.Tests\Aspire.Hosting.Azure.Tests.csproj (Aspire.Hosting.Azure.Tests)
// 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);
}