File: AzureEnvironmentResourceTests.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 ASPIREAZURE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
using Azure.Provisioning;
using Azure.Provisioning.Storage;
using Microsoft.DotNet.RemoteExecutor;
 
namespace Aspire.Hosting.Azure.Tests;
 
public class AzureEnvironmentResourceTests(ITestOutputHelper output)
{
    [Fact]
    public async Task WhenUsedWithAzureContainerAppsEnvironment_GeneratesProperBicep()
    {
        // Arrange
        var tempDir = Directory.CreateTempSubdirectory(".azure-environment-resource-test");
        output.WriteLine($"Temp directory: {tempDir.FullName}");
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, tempDir.FullName);
 
        var containerAppEnv = builder.AddAzureContainerAppEnvironment("env");
 
        // Add a container that will use the container app environment
        builder.AddContainer("api", "my-api-image:latest")
            .WithHttpEndpoint();
 
        // Act
        using var app = builder.Build();
        app.Run();
 
        var mainBicepPath = Path.Combine(tempDir.FullName, "main.bicep");
        Assert.True(File.Exists(mainBicepPath));
        var mainBicep = File.ReadAllText(mainBicepPath);
 
        var envBicepPath = Path.Combine(tempDir.FullName, "env", "env.bicep");
        Assert.True(File.Exists(envBicepPath));
        var envBicep = File.ReadAllText(envBicepPath);
 
        await Verify(mainBicep, "bicep")
            .AppendContentAsFile(envBicep, "bicep");
 
        tempDir.Delete(recursive: true);
    }
 
    [Fact]
    public async Task WhenUsedWithAzureContainerAppsEnvironment_RespectsStronglyTypedProperties()
    {
        // Arrange
        var tempDir = Directory.CreateTempSubdirectory(".azure-environment-resource-test");
        output.WriteLine($"Temp directory: {tempDir.FullName}");
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, tempDir.FullName);
 
        var locationParam = builder.AddParameter("location", "eastus2");
        var resourceGroupParam = builder.AddParameter("resourceGroup", "my-rg");
        builder.AddAzureEnvironment()
            .WithLocation(locationParam)
            .WithResourceGroup(resourceGroupParam);
        var containerAppEnv = builder.AddAzureContainerAppEnvironment("env");
 
        // Add a container that will use the container app environment
        builder.AddContainer("api", "my-api-image:latest")
            .WithHttpEndpoint();
 
        // Act
        using var app = builder.Build();
        app.Run();
 
        var mainBicepPath = Path.Combine(tempDir.FullName, "main.bicep");
        Assert.True(File.Exists(mainBicepPath));
        var mainBicep = File.ReadAllText(mainBicepPath);
 
        await Verify(mainBicep, "bicep");
 
        tempDir.Delete(recursive: true);
    }
 
    [Fact]
    public async Task PublishAsync_GeneratesMainBicep_WithSnapshots()
    {
        // Arrange
        var tempDir = Directory.CreateTempSubdirectory(".azure-environment-resource-test");
        output.WriteLine($"Temp directory: {tempDir.FullName}");
        var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish,
            tempDir.FullName);
 
        builder.AddAzureContainerAppEnvironment("acaEnv");
 
        var storageSku = builder.AddParameter("storageSku", "Standard_LRS", publishValueAsDefault: true);
        var description = builder.AddParameter("skuDescription", "The sku is ", publishValueAsDefault: true);
        var skuDescriptionExpr = ReferenceExpression.Create($"{description} {storageSku}");
        var kvName = builder.AddParameter("kvName");
        var kvRg = builder.AddParameter("kvRg", "rg-shared");
        builder.AddAzureKeyVault("kv").AsExisting(kvName, kvRg);
        builder.AddAzureStorage("existing-storage").PublishAsExisting("images", "rg-shared");
        var pgdb = builder.AddAzurePostgresFlexibleServer("pg").AddDatabase("pgdb");
        var cosmos = builder.AddAzureCosmosDB("account").AddCosmosDatabase("db");
        var blobs = builder.AddAzureStorage("storage")
            .ConfigureInfrastructure(c =>
            {
                var storageAccount = c.GetProvisionableResources().OfType<StorageAccount>().FirstOrDefault();
                storageAccount!.Sku.Name = storageSku.AsProvisioningParameter(c);
                var output = new ProvisioningOutput("description", typeof(string))
                {
                    Value = skuDescriptionExpr.AsProvisioningParameter(c, "sku_description")
                };
                c.Add(output);
            })
            .AddBlobs("blobs");
        builder.AddAzureInfrastructure("mod", infra => { })
            .WithParameter("pgdb", pgdb.Resource.ConnectionStringExpression);
        builder.AddContainer("myapp", "mcr.microsoft.com/dotnet/aspnet:8.0")
                        .WithReference(cosmos);
        builder.AddProject<TestProject>("fe", launchProfileName: null)
                        .WithEnvironment("BLOB_CONTAINER_URL", $"{blobs}/container");
 
        var app = builder.Build();
        app.Run();
 
        var mainBicepPath = Path.Combine(tempDir.FullName, "main.bicep");
        Assert.True(File.Exists(mainBicepPath));
        var content = File.ReadAllText(mainBicepPath);
 
        await Verify(content, extension: "bicep");
    }
 
    [Fact]
    public async Task AzurePublishingContext_CapturesParametersAndOutputsCorrectly_WithSnapshot()
    {
        var tempDir = Directory.CreateTempSubdirectory(".azure-environment-resource-test");
        output.WriteLine($"Temp directory: {tempDir.FullName}");
        var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish,
            tempDir.FullName);
        builder.AddAzureContainerAppEnvironment("acaEnv");
        var storageSku = builder.AddParameter("storage-Sku", "Standard_LRS", publishValueAsDefault: true);
        var description = builder.AddParameter("skuDescription", "The sku is ", publishValueAsDefault: true);
        var skuDescriptionExpr = ReferenceExpression.Create($"{description} {storageSku}");
        var kv = builder.AddAzureKeyVault("kv");
        var cosmos = builder.AddAzureCosmosDB("account").AddCosmosDatabase("db");
        var blobs = builder.AddAzureStorage("storage")
            .ConfigureInfrastructure(c =>
            {
                var storageAccount = c.GetProvisionableResources().OfType<StorageAccount>().FirstOrDefault();
                storageAccount!.Sku.Name = storageSku.AsProvisioningParameter(c);
                var output = new ProvisioningOutput("description", typeof(string))
                {
                    Value = skuDescriptionExpr.AsProvisioningParameter(c, "sku_description")
                };
                c.Add(output);
            })
            .AddBlobs("blobs");
        builder.AddProject<TestProject>("fe", launchProfileName: null)
            .WithEnvironment("BLOB_CONTAINER_URL", $"{blobs}/container")
            .WithReference(cosmos);
        var externalResource = new ExternalResourceWithParameters("external")
        {
            Parameters =
            {
                ["kvUri"] = kv.Resource.VaultUri,
                ["blob"] = blobs.Resource.ConnectionStringExpression,
            }
        };
        builder.AddResource(externalResource);
 
        var app = builder.Build();
        app.Run();
 
        var mainBicep = File.ReadAllText(Path.Combine(tempDir.FullName, "main.bicep"));
        var storageBicep = File.ReadAllText(Path.Combine(tempDir.FullName, "storage", "storage.bicep"));
 
        await Verify(mainBicep, "bicep")
            .AppendContentAsFile(storageBicep, "bicep");
    }
 
    private sealed class TestProject : IProjectMetadata
    {
        public string ProjectPath => "another-path";
 
        public LaunchSettings? LaunchSettings { get; set; }
    }
 
    [Fact]
    public async Task AzurePublishingContext_IgnoresAzureBicepResourcesWithIgnoreAnnotation()
    {
        // Arrange
        using var tempDir = new TestTempDirectory();
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish,
            tempDir.Path);
 
        // Add an Azure storage resource that will be included
        var includedStorage = builder.AddAzureStorage("included-storage");
 
        // Add an Azure storage resource that will be excluded
        var excludedStorage = builder.AddAzureStorage("excluded-storage")
            .ExcludeFromManifest(); // This should be ignored during publishing
 
        // Act
        using var app = builder.Build();
        app.Run();
 
        // Assert - Verify the generated bicep files
        var mainBicepPath = Path.Combine(tempDir.Path, "main.bicep");
        Assert.True(File.Exists(mainBicepPath));
        var mainBicep = File.ReadAllText(mainBicepPath);
 
        // Check if included-storage bicep file was generated
        var includedStorageBicepPath = Path.Combine(tempDir.Path, "included-storage", "included-storage.bicep");
        Assert.True(File.Exists(includedStorageBicepPath), "Included storage should have a bicep file generated");
 
        // Verify that excluded-storage bicep file was NOT generated
        var excludedStorageBicepPath = Path.Combine(tempDir.Path, "excluded-storage", "excluded-storage.bicep");
        Assert.False(File.Exists(excludedStorageBicepPath), "Excluded storage should not have a bicep file generated");
 
        await Verify(mainBicep, "bicep");
    }
 
    [Fact]
    public async Task PublishAsync_WithDockerfileFactory_WritesDockerfileToOutputFolder()
    {
        using var tempDir = new TestTempDirectory();
        var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, tempDir.Path);
 
        var containerAppEnv = builder.AddAzureContainerAppEnvironment("env");
 
        var dockerfileContent = "FROM alpine:latest\nRUN echo 'Generated for azure'";
        var container = builder.AddContainer("testcontainer", "testimage")
                               .WithDockerfileFactory(".", context => dockerfileContent);
 
        var app = builder.Build();
        app.Run();
 
        // Verify Dockerfile was written to resource-specific path
        var dockerfilePath = Path.Combine(tempDir.Path, "testcontainer.Dockerfile");
        Assert.True(File.Exists(dockerfilePath), $"Dockerfile should exist at {dockerfilePath}");
        var actualContent = await File.ReadAllTextAsync(dockerfilePath);
 
        await Verify(actualContent);
    }
 
    [Fact]
    public void AzurePublishingContext_WithBicepTemplateFile_WorksWithRelativePath()
    {
        using var testTempDir = new TestTempDirectory();
 
        var remoteInvokeOptions = new RemoteInvokeOptions();
        remoteInvokeOptions.StartInfo.WorkingDirectory = testTempDir.Path;
        RemoteExecutor.Invoke(RunTest, testTempDir.Path, remoteInvokeOptions).Dispose();
 
        static async Task RunTest(string tempDir)
        {
            // This test verifies the fix for https://github.com/dotnet/aspire/issues/13967
            // When using AzureBicepResource with a relative templateFile and AzurePublishingContext,
            // the bicep file should be correctly copied to the output directory.
 
            // Create a source bicep file (simulating a user's custom bicep template)
            var bicepFileName = "custom-resource.bicep";
            var bicepFilePath = Path.Combine(tempDir, bicepFileName);
            var bicepContent = """
            param location string = resourceGroup().location
            param customName string
 
            resource storageAccount 'Microsoft.Storage/storageAccounts@2021-02-01' = {
              name: customName
              location: location
              kind: 'StorageV2'
              sku: {
                name: 'Standard_LRS'
              }
            }
 
            output endpoint string = storageAccount.properties.primaryEndpoints.blob
            """;
            await File.WriteAllTextAsync(bicepFilePath, bicepContent);
 
            // Create output directory for publishing
            var outputDir = Path.Combine(tempDir, "output");
            Directory.CreateDirectory(outputDir);
 
            var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputPath: outputDir);
 
            // Add a container app environment (required for publishing)
            builder.AddAzureContainerAppEnvironment("env");
 
            // Add the custom AzureBicepResource with a relative template file path
            var customResource = new AzureBicepResource("custom-resource", bicepFileName);
            builder.AddResource(customResource)
                .WithParameter("customName", "mystorageaccount");
 
            var app = builder.Build();
            app.Run();
 
            // Verify the bicep file was copied to the output directory
            var mainBicepPath = Path.Combine(outputDir, "main.bicep");
            Assert.True(File.Exists(mainBicepPath), "main.bicep should be generated");
 
            var resourceBicepPath = Path.Combine(outputDir, "custom-resource", "custom-resource.bicep");
            Assert.True(File.Exists(resourceBicepPath), "custom-resource/custom-resource.bicep should be generated");
 
            // Verify the content of the copied file matches the original
            var copiedContent = await File.ReadAllTextAsync(resourceBicepPath);
            Assert.Equal(bicepContent, copiedContent);
 
            // Verify the main.bicep references the resource
            var mainBicepContent = await File.ReadAllTextAsync(mainBicepPath);
            Assert.Contains("module custom_resource 'custom-resource/custom-resource.bicep'", mainBicepContent);
        }
    }
 
    private sealed class ExternalResourceWithParameters(string name) : Resource(name), IResourceWithParameters
    {
        public IDictionary<string, object?> Parameters { get; } = new Dictionary<string, object?>();
    }
}