File: AzureAppServiceEnvironmentExtensionsTests.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.
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting.Azure.Tests;
 
public class AzureAppServiceEnvironmentExtensionsTests
{
    [Fact]
    public void AddAsExistingResource_ShouldBeIdempotent_ForAzureAppServiceEnvironmentResource()
    {
        // Arrange
        var appServiceEnvironmentResource = new AzureAppServiceEnvironmentResource("test-app-service-env", _ => { });
        var infrastructure = new AzureResourceInfrastructure(appServiceEnvironmentResource, "test-app-service-env");
 
        // Act - Call AddAsExistingResource twice
        var firstResult = appServiceEnvironmentResource.AddAsExistingResource(infrastructure);
        var secondResult = appServiceEnvironmentResource.AddAsExistingResource(infrastructure);
 
        // Assert - Both calls should return the same resource instance, not duplicates
        Assert.Same(firstResult, secondResult);
    }
 
    [Fact]
    public async Task AddAsExistingResource_RespectsExistingAzureResourceAnnotation_ForAzureAppServiceEnvironmentResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var existingName = builder.AddParameter("existing-appenv-name");
        var existingResourceGroup = builder.AddParameter("existing-appenv-rg");
 
        var appServiceEnvironment = builder.AddAzureAppServiceEnvironment("test-app-service-env")
            .AsExisting(existingName, existingResourceGroup);
 
        var module = builder.AddAzureInfrastructure("mymodule", infra =>
        {
            _ = appServiceEnvironment.Resource.AddAsExistingResource(infra);
        });
 
        var (manifest, bicep) = await AzureManifestUtils.GetManifestWithBicep(module.Resource, skipPreparer: true);
 
        await Verify(manifest.ToString(), "json")
             .AppendContentAsFile(bicep, "bicep");
    }
 
    [Fact]
    public void ContainerRegistry_ReturnsDefaultContainerRegistry()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var appServiceEnvironment = builder.AddAzureAppServiceEnvironment("env");
 
        // The environment should have a default container registry set up
        var registry = appServiceEnvironment.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 appServiceEnvironment = builder.AddAzureAppServiceEnvironment("env")
            .WithAzureContainerRegistry(acr);
 
        // Should return the explicitly set registry
        var registry = appServiceEnvironment.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 AzureAppServiceEnvironmentResource("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 appServiceEnvironment = builder.AddAzureAppServiceEnvironment("env")
            .WithContainerRegistry(dockerRegistry);
 
        // Should throw because a non-Azure registry is configured
        var exception = Assert.Throws<InvalidOperationException>(() => appServiceEnvironment.Resource.ContainerRegistry);
        Assert.Contains("not an Azure Container Registry", exception.Message);
        Assert.Contains("env", exception.Message);
    }
 
    [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.AddAzureAppServiceEnvironment("env")
            .WithAzureContainerRegistry(firstRegistry)
            .WithAzureContainerRegistry(provisionedRegistry)
            .WithAzureContainerRegistry(finalRegistry)
            .WithDashboard(false);
 
        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 SameScopeExistingRegistry_KeepsInlineAcrPullIdentity()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var registry = builder.AddAzureContainerRegistry("registry")
            .PublishAsExisting("myacr", resourceGroup: null);
 
        builder.AddAzureAppServiceEnvironment("env")
            .WithAzureContainerRegistry(registry)
            .WithDashboard(false);
 
        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.AddAzureAppServiceEnvironment("env")
            .WithAzureContainerRegistry(registry)
            .WithDashboard(false);
        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 so the two environments resolve to distinct managed
        // environment names within one resource group, mirroring the Container Apps test.
        builder.AddAzureAppServiceEnvironment("east")
            .WithAzureContainerRegistry(registry)
            .WithDashboard(false);
        builder.AddAzureAppServiceEnvironment("west")
            .WithAzureContainerRegistry(registry)
            .WithDashboard(false);
 
        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);
    }
}