File: AzurePublisherTests.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.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Xunit;
using static Aspire.Hosting.Utils.AzureManifestUtils;
 
namespace Aspire.Hosting.Azure.Tests;
 
public class AzurePublisherTests(ITestOutputHelper output)
{
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task PublishAsync_GeneratesMainBicep(bool useContext)
    {
        using var tempDirectory = new TempDirectory();
        using var tempDir = new TempDirectory();
        // Arrange
        var options = new OptionsMonitor(new AzurePublisherOptions { OutputPath = tempDir.Path });
        var provisionerOptions = Options.Create(new AzureProvisioningOptions());
 
        var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        // The azure publisher is not tied to azure container apps but this is 
        // a good way to test the end to end scenario
        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 =>
        {
            // Noop
        })
        .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();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        await ExecuteBeforeStartHooksAsync(app, default);
 
        if (useContext)
        {
            // tests the public AzurePublishingContext API
            var context = new AzurePublishingContext(
                options.CurrentValue,
                provisionerOptions.Value,
                NullLogger<AzurePublishingContext>.Instance);
 
            await context.WriteModelAsync(model, default);
        }
        else
        {
            // tests via the internal Publisher object
            var publisher = new AzurePublisher("azure",
                options,
                provisionerOptions,
                NullLogger<AzurePublisher>.Instance);
 
            await publisher.PublishAsync(model, default);
        }
 
        Assert.True(File.Exists(Path.Combine(tempDir.Path, "main.bicep")));
 
        var content = File.ReadAllText(Path.Combine(tempDir.Path, "main.bicep"));
 
        var expectedBicep = """
            targetScope = 'subscription'
 
            param environmentName string
 
            param location string
 
            param principalId string
 
            param kvRg string
 
            param kvName string
 
            param storageSku string = 'Standard_LRS'
 
            param skuDescription string = 'The sku is '
 
            var tags = {
              'aspire-env-name': environmentName
            }
 
            resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = {
              name: 'rg-${environmentName}'
              location: location
              tags: tags
            }
 
            module acaEnv 'acaEnv/acaEnv.bicep' = {
              name: 'acaEnv'
              scope: rg
              params: {
                location: location
                userPrincipalId: principalId
              }
            }
 
            module kv 'kv/kv.bicep' = {
              name: 'kv'
              scope: resourceGroup(kvRg)
              params: {
                location: location
                kvName: kvName
              }
            }
 
            module existing_storage 'existing-storage/existing-storage.bicep' = {
              name: 'existing-storage'
              scope: resourceGroup('rg-shared')
              params: {
                location: location
              }
            }
 
            module pg 'pg/pg.bicep' = {
              name: 'pg'
              scope: rg
              params: {
                location: location
              }
            }
 
            module account 'account/account.bicep' = {
              name: 'account'
              scope: rg
              params: {
                location: location
              }
            }
 
            module storage 'storage/storage.bicep' = {
              name: 'storage'
              scope: rg
              params: {
                location: location
                storageSku: storageSku
                sku_description: '${skuDescription} ${storageSku}'
              }
            }
 
            module mod 'mod/mod.bicep' = {
              name: 'mod'
              scope: rg
              params: {
                location: location
                pgdb: '${pg.outputs.connectionString};Database=pgdb'
              }
            }
 
            module myapp_identity 'myapp-identity/myapp-identity.bicep' = {
              name: 'myapp-identity'
              scope: rg
              params: {
                location: location
              }
            }
 
            module myapp_roles_account 'myapp-roles-account/myapp-roles-account.bicep' = {
              name: 'myapp-roles-account'
              scope: rg
              params: {
                location: location
                account_outputs_name: account.outputs.name
                principalId: myapp_identity.outputs.principalId
              }
            }
 
            module fe_identity 'fe-identity/fe-identity.bicep' = {
              name: 'fe-identity'
              scope: rg
              params: {
                location: location
              }
            }
 
            module fe_roles_storage 'fe-roles-storage/fe-roles-storage.bicep' = {
              name: 'fe-roles-storage'
              scope: rg
              params: {
                location: location
                storage_outputs_name: storage.outputs.name
                principalId: fe_identity.outputs.principalId
              }
            }
 
            output myapp_identity_id string = myapp_identity.outputs.id
 
            output myapp_identity_clientId string = myapp_identity.outputs.clientId
 
            output account_connectionString string = account.outputs.connectionString
 
            output acaEnv_AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = acaEnv.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN
 
            output acaEnv_AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = acaEnv.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID
 
            output fe_identity_id string = fe_identity.outputs.id
 
            output fe_identity_clientId string = fe_identity.outputs.clientId
 
            output storage_blobEndpoint string = storage.outputs.blobEndpoint
 
            output acaEnv_AZURE_CONTAINER_REGISTRY_ENDPOINT string = acaEnv.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT
 
            output acaEnv_AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = acaEnv.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID
            """;
        output.WriteLine(content);
        Assert.Equal(expectedBicep, content, ignoreAllWhiteSpace: true, ignoreLineEndingDifferences: true);
    }
 
    private sealed class OptionsMonitor(AzurePublisherOptions options) : IOptionsMonitor<AzurePublisherOptions>
    {
        public AzurePublisherOptions Get(string? name) => options;
 
        public IDisposable OnChange(Action<AzurePublisherOptions, string> listener) => null!;
 
        public AzurePublisherOptions CurrentValue => options;
    }
 
    private sealed class TempDirectory : IDisposable
    {
        public TempDirectory()
        {
            Path = Directory.CreateTempSubdirectory(".aspire-publisers").FullName;
        }
 
        public string Path { get; }
        public void Dispose()
        {
            if (File.Exists(Path))
            {
                File.Delete(Path);
            }
        }
    }
 
    private sealed class TestProject : IProjectMetadata
    {
        public string ProjectPath => "another-path";
 
        public LaunchSettings? LaunchSettings { get; set; }
    }
}