File: AzureBicepResourceTests.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 AZPROVISION001 // Because we are testing CDK callbacks.
 
using System.Text.Json.Nodes;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Azure.Provisioning;
using Azure.Provisioning.Roles;
using Azure.Provisioning.CognitiveServices;
using Azure.Provisioning.CosmosDB;
using Azure.Provisioning.KeyVault;
using Azure.Provisioning.Storage;
using Azure.Provisioning.Sql;
using Azure.Provisioning.Expressions;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Xunit.Abstractions;
 
namespace Aspire.Hosting.Azure.Tests;
 
public class AzureBicepResourceTests(ITestOutputHelper output)
{
    [Fact]
    public void AddBicepResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var bicepResource = builder.AddBicepTemplateString("mytemplate", "content")
                                   .WithParameter("param1", "value1")
                                   .WithParameter("param2", "value2");
 
        Assert.Equal("content", bicepResource.Resource.TemplateString);
        Assert.Equal("value1", bicepResource.Resource.Parameters["param1"]);
        Assert.Equal("value2", bicepResource.Resource.Parameters["param2"]);
    }
 
    public static TheoryData<Func<IDistributedApplicationBuilder, IResourceBuilder<IResource>>> AzureExtensions
    {
 
        get
        {
            static void CreateConstruct(ResourceModuleConstruct construct)
            {
                var id = new UserAssignedIdentity("id");
                construct.Add(id);
                construct.Add(new BicepOutput("cid", typeof(string)) { Value = id.ClientId });
            }
 
            return new()
            {
                { builder => builder.AddAzureAppConfiguration("x") },
                { builder => builder.AddAzureApplicationInsights("x") },
                { builder => builder.AddBicepTemplate("x", "template.bicep") },
                { builder => builder.AddBicepTemplateString("x", "content") },
                { builder => builder.AddAzureConstruct("x", CreateConstruct) },
                { builder => builder.AddAzureOpenAI("x") },
                { builder => builder.AddAzureCosmosDB("x") },
                { builder => builder.AddAzureEventHubs("x") },
                { builder => builder.AddAzureKeyVault("x") },
                { builder => builder.AddAzureLogAnalyticsWorkspace("x") },
                { builder => builder.AddPostgres("x").AsAzurePostgresFlexibleServer() },
                { builder => builder.AddRedis("x").AsAzureRedis() },
                { builder => builder.AddAzureSearch("x") },
                { builder => builder.AddAzureServiceBus("x") },
                { builder => builder.AddAzureSignalR("x") },
                { builder => builder.AddSqlServer("x").AsAzureSqlDatabase() },
                { builder => builder.AddAzureStorage("x") },
            };
        }
    }
 
    [Theory]
    [MemberData(nameof(AzureExtensions))]
    public void AzureExtensionsAutomaticallyAddAzureProvisioning(Func<IDistributedApplicationBuilder, IResourceBuilder<IResource>> addAzureResource)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        addAzureResource(builder);
 
        var app = builder.Build();
        var hooks = app.Services.GetServices<IDistributedApplicationLifecycleHook>();
        Assert.Single(hooks.OfType<AzureProvisioner>());
    }
 
    [Theory]
    [MemberData(nameof(AzureExtensions))]
    public void BicepResourcesAreIdempotent(Func<IDistributedApplicationBuilder, IResourceBuilder<IResource>> addAzureResource)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var azureResourceBuilder = addAzureResource(builder);
 
        if (azureResourceBuilder.Resource is not AzureConstructResource bicepResource)
        {
            // Skip
            return;
        }
 
        // This makes sure that these don't throw
        bicepResource.GetBicepTemplateFile();
        bicepResource.GetBicepTemplateFile();
    }
 
    [Fact]
    public void GetOutputReturnsOutputValue()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var bicepResource = builder.AddBicepTemplateString("templ", "content");
 
        bicepResource.Resource.Outputs["resourceEndpoint"] = "https://myendpoint";
 
        Assert.Equal("https://myendpoint", bicepResource.GetOutput("resourceEndpoint").Value);
    }
 
    [Fact]
    public void GetSecretOutputReturnsSecretOutputValue()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var bicepResource = builder.AddBicepTemplateString("templ", "content");
 
        bicepResource.Resource.SecretOutputs["connectionString"] = "https://myendpoint;Key=43";
 
        Assert.Equal("https://myendpoint;Key=43", bicepResource.GetSecretOutput("connectionString").Value);
    }
 
    [Fact]
    public void GetOutputValueThrowsIfNoOutput()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var bicepResource = builder.AddBicepTemplateString("templ", "content");
 
        Assert.Throws<InvalidOperationException>(() => bicepResource.GetOutput("resourceEndpoint").Value);
    }
 
    [Fact]
    public void GetSecretOutputValueThrowsIfNoOutput()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var bicepResource = builder.AddBicepTemplateString("templ", "content");
 
        Assert.Throws<InvalidOperationException>(() => bicepResource.GetSecretOutput("connectionString").Value);
    }
 
    [Fact]
    public async Task AssertManifestLayout()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var param = builder.AddParameter("p1");
 
        var b2 = builder.AddBicepTemplateString("temp2", "content");
 
        var bicepResource = builder.AddBicepTemplateString("templ", "content")
                                    .WithParameter("param1", "value1")
                                    .WithParameter("param2", ["1", "2"])
                                    .WithParameter("param3", new JsonObject() { ["value"] = "nested" })
                                    .WithParameter("param4", param)
                                    .WithParameter("param5", b2.GetOutput("value1"))
                                    .WithParameter("param6", () => b2.GetOutput("value2"));
 
        bicepResource.Resource.TempDirectory = Environment.CurrentDirectory;
 
        var manifest = await ManifestUtils.GetManifest(bicepResource.Resource);
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "path": "templ.module.bicep",
              "params": {
                "param1": "value1",
                "param2": [
                  "1",
                  "2"
                ],
                "param3": {
                  "value": "nested"
                },
                "param4": "{p1.value}",
                "param5": "{temp2.outputs.value1}",
                "param6": "{temp2.outputs.value2}"
              }
            }
            """{
              "type": "azure.bicep.v0",
              "path": "templ.module.bicep",
              "params": {
                "param1": "value1",
                "param2": [
                  "1",
                  "2"
                ],
                "param3": {
                  "value": "nested"
                },
                "param4": "{p1.value}",
                "param5": "{temp2.outputs.value1}",
                "param6": "{temp2.outputs.value2}"
              }
            }
            """;
 
        Assert.Equal(expectedManifest, manifest.ToString());
    }
 
    [Fact]
    public async Task AddAzureCosmosDBEmulator()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var cosmos = builder.AddAzureCosmosDB("cosmos").RunAsEmulator(e =>
        {
            e.WithEndpoint("emulator", e => e.AllocatedEndpoint = new(e, "localost", 10001));
        });
 
        Assert.True(cosmos.Resource.IsContainer());
 
        var cs = AzureCosmosDBEmulatorConnectionString.Create(10001);
 
        Assert.Equal(cs, cosmos.Resource.ConnectionStringExpression.ValueExpression);
        Assert.Equal(cs, await ((IResourceWithConnectionString)cosmos.Resource).GetConnectionStringAsync());
    }
 
    [Fact]
    public async Task AddAzureCosmosDBViaRunMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        IEnumerable<CosmosDBSqlDatabase>? callbackDatabases = null;
        var cosmos = builder.AddAzureCosmosDB("cosmos", (resource, construct, account, databases) =>
        {
            callbackDatabases = databases;
        });
        cosmos.AddDatabase("mydatabase");
 
        cosmos.Resource.SecretOutputs["connectionString"] = "mycosmosconnectionstring";
 
        var manifest = await ManifestUtils.GetManifestWithBicep(cosmos.Resource);
 
        var expectedManifest = """
                               {
                                 "type": "azure.bicep.v0",
                                 "connectionString": "{cosmos.secretOutputs.connectionString}",
                                 "path": "cosmos.module.bicep",
                                 "params": {
                                   "keyVaultName": ""
                                 }
                               }
                               """{
                                 "type": "azure.bicep.v0",
                                 "connectionString": "{cosmos.secretOutputs.connectionString}",
                                 "path": "cosmos.module.bicep",
                                 "params": {
                                   "keyVaultName": ""
                                 }
                               }
                               """;
        Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param keyVaultName string
 
            resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' existing = {
                name: keyVaultName
            }
 
            resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2024-05-15-preview' = {
                name: toLower(take('cosmos${uniqueString(resourceGroup().id)}', 24))
                location: location
                properties: {
                    locations: [
                        {
                            locationName: location
                            failoverPriority: 0
                        }
                    ]
                    consistencyPolicy: {
                        defaultConsistencyLevel: 'Session'
                    }
                    databaseAccountOfferType: 'Standard'
                }
                kind: 'GlobalDocumentDB'
                tags: {
                    'aspire-resource-name': 'cosmos'
                }
            }
 
            resource mydatabase 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2024-05-15-preview' = {
                name: 'mydatabase'
                location: location
                properties: {
                    resource: {
                        id: 'mydatabase'
                    }
                }
                parent: cosmos
            }
 
            resource connectionString 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = {
                name: 'connectionString'
                properties: {
                    value: 'AccountEndpoint=${cosmos.properties.documentEndpoint};AccountKey=${cosmos.listKeys().primaryMasterKey}'
                }
                parent: keyVault
            }
            """;
        output.WriteLine(manifest.BicepText);
        Assert.Equal(expectedBicep, manifest.BicepText);
 
        Assert.NotNull(callbackDatabases);
        Assert.Collection(
            callbackDatabases,
            (database) => Assert.Equal("mydatabase", database.Name.Value)
            );
 
        var connectionStringResource = (IResourceWithConnectionString)cosmos.Resource;
 
        Assert.Equal("cosmos", cosmos.Resource.Name);
        Assert.Equal("mycosmosconnectionstring", await connectionStringResource.GetConnectionStringAsync());
    }
 
    [Fact]
    public async Task AddAzureCosmosDBViaPublishMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        IEnumerable<CosmosDBSqlDatabase>? callbackDatabases = null;
        var cosmos = builder.AddAzureCosmosDB("cosmos", (resource, construct, account, databases) =>
        {
            callbackDatabases = databases;
        });
        cosmos.AddDatabase("mydatabase");
 
        cosmos.Resource.SecretOutputs["connectionString"] = "mycosmosconnectionstring";
 
        var manifest = await ManifestUtils.GetManifestWithBicep(cosmos.Resource);
 
        var expectedManifest = """
                               {
                                 "type": "azure.bicep.v0",
                                 "connectionString": "{cosmos.secretOutputs.connectionString}",
                                 "path": "cosmos.module.bicep",
                                 "params": {
                                   "keyVaultName": ""
                                 }
                               }
                               """{
                                 "type": "azure.bicep.v0",
                                 "connectionString": "{cosmos.secretOutputs.connectionString}",
                                 "path": "cosmos.module.bicep",
                                 "params": {
                                   "keyVaultName": ""
                                 }
                               }
                               """;
        Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param keyVaultName string
 
            resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' existing = {
                name: keyVaultName
            }
 
            resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2024-05-15-preview' = {
                name: toLower(take('cosmos${uniqueString(resourceGroup().id)}', 24))
                location: location
                properties: {
                    locations: [
                        {
                            locationName: location
                            failoverPriority: 0
                        }
                    ]
                    consistencyPolicy: {
                        defaultConsistencyLevel: 'Session'
                    }
                    databaseAccountOfferType: 'Standard'
                }
                kind: 'GlobalDocumentDB'
                tags: {
                    'aspire-resource-name': 'cosmos'
                }
            }
 
            resource mydatabase 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2024-05-15-preview' = {
                name: 'mydatabase'
                location: location
                properties: {
                    resource: {
                        id: 'mydatabase'
                    }
                }
                parent: cosmos
            }
 
            resource connectionString 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = {
                name: 'connectionString'
                properties: {
                    value: 'AccountEndpoint=${cosmos.properties.documentEndpoint};AccountKey=${cosmos.listKeys().primaryMasterKey}'
                }
                parent: keyVault
            }
            """;
        output.WriteLine(manifest.BicepText);
        Assert.Equal(expectedBicep, manifest.BicepText);
 
        Assert.NotNull(callbackDatabases);
        Assert.Collection(
            callbackDatabases,
            (database) => Assert.Equal("mydatabase", database.Name.Value)
            );
 
        var connectionStringResource = (IResourceWithConnectionString)cosmos.Resource;
 
        Assert.Equal("cosmos", cosmos.Resource.Name);
        Assert.Equal("mycosmosconnectionstring", await connectionStringResource.GetConnectionStringAsync());
    }
 
    [Fact]
    public async Task AddAzureAppConfiguration()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var appConfig = builder.AddAzureAppConfiguration("appConfig");
        appConfig.Resource.Outputs["appConfigEndpoint"] = "https://myendpoint";
        Assert.Equal("https://myendpoint", await appConfig.Resource.ConnectionStringExpression.GetValueAsync(default));
 
        var manifest = await ManifestUtils.GetManifestWithBicep(appConfig.Resource);
 
        var connectionStringResource = (IResourceWithConnectionString)appConfig.Resource;
 
        Assert.Equal("https://myendpoint", await connectionStringResource.GetConnectionStringAsync());
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "{appConfig.outputs.appConfigEndpoint}",
              "path": "appConfig.module.bicep",
              "params": {
                "principalId": "",
                "principalType": ""
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "{appConfig.outputs.appConfigEndpoint}",
              "path": "appConfig.module.bicep",
              "params": {
                "principalId": "",
                "principalType": ""
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param principalId string
 
            param principalType string
 
            resource appConfig 'Microsoft.AppConfiguration/configurationStores@2019-10-01' = {
                name: toLower(take('appConfig${uniqueString(resourceGroup().id)}', 24))
                location: location
                properties: {
                    disableLocalAuth: true
                }
                sku: {
                    name: 'standard'
                }
                tags: {
                    'aspire-resource-name': 'appConfig'
                }
            }
 
            resource appConfig_AppConfigurationDataOwner 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(appConfig.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5ae67dd6-50cb-40e7-96ff-dc2bfa4b606b'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5ae67dd6-50cb-40e7-96ff-dc2bfa4b606b')
                    principalType: principalType
                }
                scope: appConfig
            }
 
            output appConfigEndpoint string = appConfig.properties.endpoint
            """;
        output.WriteLine(manifest.BicepText);
        Assert.Equal(expectedBicep, manifest.BicepText);
    }
 
    [Fact]
    public async Task AddApplicationInsightsWithoutExplicitLawGetsDefaultLawParameterInPublishMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var appInsights = builder.AddAzureApplicationInsights("appInsights");
 
        appInsights.Resource.Outputs["appInsightsConnectionString"] = "myinstrumentationkey";
 
        var connectionStringResource = (IResourceWithConnectionString)appInsights.Resource;
 
        Assert.Equal("appInsights", appInsights.Resource.Name);
        Assert.Equal("myinstrumentationkey", await connectionStringResource.GetConnectionStringAsync());
        Assert.Equal("{appInsights.outputs.appInsightsConnectionString}", appInsights.Resource.ConnectionStringExpression.ValueExpression);
 
        var appInsightsManifest = await ManifestUtils.GetManifestWithBicep(appInsights.Resource);
        var expectedManifest = """
           {
             "type": "azure.bicep.v0",
             "connectionString": "{appInsights.outputs.appInsightsConnectionString}",
             "path": "appInsights.module.bicep",
             "params": {
               "logAnalyticsWorkspaceId": ""
             }
           }
           """{
             "type": "azure.bicep.v0",
             "connectionString": "{appInsights.outputs.appInsightsConnectionString}",
             "path": "appInsights.module.bicep",
             "params": {
               "logAnalyticsWorkspaceId": ""
             }
           }
           """;
        Assert.Equal(expectedManifest, appInsightsManifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param applicationType string = 'web'
 
            param kind string = 'web'
 
            param logAnalyticsWorkspaceId string
 
            resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
                name: toLower(take('appInsights${uniqueString(resourceGroup().id)}', 24))
                kind: kind
                location: location
                properties: {
                    Application_Type: applicationType
                    WorkspaceResourceId: logAnalyticsWorkspaceId
                }
                tags: {
                    'aspire-resource-name': 'appInsights'
                }
            }
 
            output appInsightsConnectionString string = appInsights.properties.ConnectionString
            """;
        output.WriteLine(appInsightsManifest.BicepText);
        Assert.Equal(expectedBicep, appInsightsManifest.BicepText);
    }
 
    [Fact]
    public async Task AddApplicationInsightsWithoutExplicitLawGetsDefaultLawParameterInRunMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var appInsights = builder.AddAzureApplicationInsights("appInsights");
 
        appInsights.Resource.Outputs["appInsightsConnectionString"] = "myinstrumentationkey";
 
        var connectionStringResource = (IResourceWithConnectionString)appInsights.Resource;
 
        Assert.Equal("appInsights", appInsights.Resource.Name);
        Assert.Equal("myinstrumentationkey", await connectionStringResource.GetConnectionStringAsync());
        Assert.Equal("{appInsights.outputs.appInsightsConnectionString}", appInsights.Resource.ConnectionStringExpression.ValueExpression);
 
        var appInsightsManifest = await ManifestUtils.GetManifestWithBicep(appInsights.Resource);
        var expectedManifest = """
           {
             "type": "azure.bicep.v0",
             "connectionString": "{appInsights.outputs.appInsightsConnectionString}",
             "path": "appInsights.module.bicep"
           }
           """{
             "type": "azure.bicep.v0",
             "connectionString": "{appInsights.outputs.appInsightsConnectionString}",
             "path": "appInsights.module.bicep"
           }
           """;
        Assert.Equal(expectedManifest, appInsightsManifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param applicationType string = 'web'
 
            param kind string = 'web'
 
            resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
                name: toLower(take('appInsights${uniqueString(resourceGroup().id)}', 24))
                kind: kind
                location: location
                properties: {
                    Application_Type: applicationType
                    WorkspaceResourceId: law-appInsights.id
                }
                tags: {
                    'aspire-resource-name': 'appInsights'
                }
            }
 
            resource law-appInsights 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
                name: toLower(take('law-appInsights${uniqueString(resourceGroup().id)}', 24))
                location: location
                properties: {
                    sku: {
                        name: 'PerGB2018'
                    }
                }
                tags: {
                    'aspire-resource-name': 'law-appInsights'
                }
            }
 
            output appInsightsConnectionString string = appInsights.properties.ConnectionString
            """;
        output.WriteLine(appInsightsManifest.BicepText);
        Assert.Equal(expectedBicep, appInsightsManifest.BicepText);
    }
 
    [Fact]
    public async Task AddApplicationInsightsWithExplicitLawArgumentDoesntGetDefaultParameter()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var law = builder.AddAzureLogAnalyticsWorkspace("mylaw");
        var appInsights = builder.AddAzureApplicationInsights("appInsights", law);
 
        appInsights.Resource.Outputs["appInsightsConnectionString"] = "myinstrumentationkey";
 
        var connectionStringResource = (IResourceWithConnectionString)appInsights.Resource;
 
        Assert.Equal("appInsights", appInsights.Resource.Name);
        Assert.Equal("myinstrumentationkey", await connectionStringResource.GetConnectionStringAsync());
        Assert.Equal("{appInsights.outputs.appInsightsConnectionString}", appInsights.Resource.ConnectionStringExpression.ValueExpression);
 
        var appInsightsManifest = await ManifestUtils.GetManifestWithBicep(appInsights.Resource);
        var expectedManifest = """
           {
             "type": "azure.bicep.v0",
             "connectionString": "{appInsights.outputs.appInsightsConnectionString}",
             "path": "appInsights.module.bicep",
             "params": {
               "logAnalyticsWorkspaceId": "{mylaw.outputs.logAnalyticsWorkspaceId}"
             }
           }
           """{
             "type": "azure.bicep.v0",
             "connectionString": "{appInsights.outputs.appInsightsConnectionString}",
             "path": "appInsights.module.bicep",
             "params": {
               "logAnalyticsWorkspaceId": "{mylaw.outputs.logAnalyticsWorkspaceId}"
             }
           }
           """;
        Assert.Equal(expectedManifest, appInsightsManifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param applicationType string = 'web'
 
            param kind string = 'web'
 
            param logAnalyticsWorkspaceId string
 
            resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
                name: toLower(take('appInsights${uniqueString(resourceGroup().id)}', 24))
                kind: kind
                location: location
                properties: {
                    Application_Type: applicationType
                    WorkspaceResourceId: logAnalyticsWorkspaceId
                }
                tags: {
                    'aspire-resource-name': 'appInsights'
                }
            }
 
            output appInsightsConnectionString string = appInsights.properties.ConnectionString
            """;
        output.WriteLine(appInsightsManifest.BicepText);
        Assert.Equal(expectedBicep, appInsightsManifest.BicepText);
    }
 
    [Fact]
    public async Task AddLogAnalyticsWorkspace()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var logAnalyticsWorkspace = builder.AddAzureLogAnalyticsWorkspace("logAnalyticsWorkspace");
 
        Assert.Equal("logAnalyticsWorkspace", logAnalyticsWorkspace.Resource.Name);
        Assert.Equal("{logAnalyticsWorkspace.outputs.logAnalyticsWorkspaceId}", logAnalyticsWorkspace.Resource.WorkspaceId.ValueExpression);
 
        var appInsightsManifest = await ManifestUtils.GetManifestWithBicep(logAnalyticsWorkspace.Resource);
        var expectedManifest = """
           {
             "type": "azure.bicep.v0",
             "path": "logAnalyticsWorkspace.module.bicep"
           }
           """{
             "type": "azure.bicep.v0",
             "path": "logAnalyticsWorkspace.module.bicep"
           }
           """;
        Assert.Equal(expectedManifest, appInsightsManifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
                name: toLower(take('logAnalyticsWorkspace${uniqueString(resourceGroup().id)}', 24))
                location: location
                properties: {
                    sku: {
                        name: 'PerGB2018'
                    }
                }
                tags: {
                    'aspire-resource-name': 'logAnalyticsWorkspace'
                }
            }
 
            output logAnalyticsWorkspaceId string = logAnalyticsWorkspace.id
            """;
        output.WriteLine(appInsightsManifest.BicepText);
        Assert.Equal(expectedBicep, appInsightsManifest.BicepText);
    }
 
    [Fact]
    public async Task WithReferenceAppInsightsSetsEnvironmentVariable()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var appInsights = builder.AddAzureApplicationInsights("ai");
 
        appInsights.Resource.Outputs["appInsightsConnectionString"] = "myinstrumentationkey";
 
        var serviceA = builder.AddProject<ProjectA>("serviceA", o => o.ExcludeLaunchProfile = true)
            .WithReference(appInsights);
 
        var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(serviceA.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance);
 
        Assert.True(config.ContainsKey("APPLICATIONINSIGHTS_CONNECTION_STRING"));
        Assert.Equal("myinstrumentationkey", config["APPLICATIONINSIGHTS_CONNECTION_STRING"]);
    }
 
    [Fact]
    public async Task AddAzureConstructGenertesCorrectManifestEntry()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var construct1 = builder.AddAzureConstruct("construct1", (construct) =>
        {
            var storage = new StorageAccount("storage")
            {
                Kind = StorageKind.StorageV2,
                Sku = new StorageSku() { Name = StorageSkuName.StandardLrs }
            };
            construct.Add(storage);
            construct.Add(new BicepOutput("storageAccountName", typeof(string)) { Value = storage.Name });
        });
 
        var manifest = await ManifestUtils.GetManifest(construct1.Resource);
        Assert.Equal("azure.bicep.v0", manifest["type"]?.ToString());
        Assert.Equal("construct1.module.bicep", manifest["path"]?.ToString());
    }
 
    [Fact]
    public async Task AssignParameterPopulatesParametersEverywhere()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Configuration["Parameters:skuName"] = "Standard_ZRS";
 
        var skuName = builder.AddParameter("skuName");
 
        ResourceModuleConstruct? moduleConstruct = null;
        var construct1 = builder.AddAzureConstruct("construct1", (construct) =>
        {
            var storage = new StorageAccount("storage")
            {
                Kind = StorageKind.StorageV2,
                Sku = new StorageSku() { Name = skuName.AsBicepParameter(construct) }
            };
            construct.Add(storage);
            moduleConstruct = construct;
        });
 
        var manifest = await ManifestUtils.GetManifest(construct1.Resource);
 
        Assert.NotNull(moduleConstruct);
        var constructParameters = moduleConstruct.GetParameters().DistinctBy(x => x.ResourceName);
        var constructParametersLookup = constructParameters.ToDictionary(p => p.ResourceName);
        Assert.True(constructParametersLookup.ContainsKey("skuName"));
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "path": "construct1.module.bicep",
              "params": {
                "skuName": "{skuName.value}"
              }
            }
            """{
              "type": "azure.bicep.v0",
              "path": "construct1.module.bicep",
              "params": {
                "skuName": "{skuName.value}"
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ToString());
    }
 
    [Fact]
    public async Task AssignParameterWithSpecifiedNamePopulatesParametersEverywhere()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Configuration["Parameters:skuName"] = "Standard_ZRS";
 
        var skuName = builder.AddParameter("skuName");
 
        ResourceModuleConstruct? moduleConstruct = null;
        var construct1 = builder.AddAzureConstruct("construct1", (construct) =>
        {
            var storage = new StorageAccount("storage")
            {
                Kind = StorageKind.StorageV2,
                Sku = new StorageSku() { Name = skuName.AsBicepParameter(construct, parameterName: "sku") }
            };
            construct.Add(storage);
            moduleConstruct = construct;
        });
 
        var manifest = await ManifestUtils.GetManifest(construct1.Resource);
 
        Assert.NotNull(moduleConstruct);
        var constructParameters = moduleConstruct.GetParameters().DistinctBy(x => x.ResourceName);
        var constructParametersLookup = constructParameters.ToDictionary(p => p.ResourceName);
        Assert.True(constructParametersLookup.ContainsKey("sku"));
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "path": "construct1.module.bicep",
              "params": {
                "sku": "{skuName.value}"
              }
            }
            """{
              "type": "azure.bicep.v0",
              "path": "construct1.module.bicep",
              "params": {
                "sku": "{skuName.value}"
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ToString());
    }
 
    [Fact]
    public async Task PublishAsRedisPublishesRedisAsAzureRedisConstruct()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var redis = builder.AddRedis("cache")
            .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 12455))
            .PublishAsAzureRedis();
 
        Assert.True(redis.Resource.IsContainer());
 
        Assert.Equal("localhost:12455", await redis.Resource.GetConnectionStringAsync());
 
        var manifest = await ManifestUtils.GetManifestWithBicep(redis.Resource);
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "{cache.secretOutputs.connectionString}",
              "path": "cache.module.bicep",
              "params": {
                "keyVaultName": ""
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "{cache.secretOutputs.connectionString}",
              "path": "cache.module.bicep",
              "params": {
                "keyVaultName": ""
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param keyVaultName string
 
            resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' existing = {
                name: keyVaultName
            }
 
            resource cache 'Microsoft.Cache/redis@2020-06-01' = {
                name: toLower(take('cache${uniqueString(resourceGroup().id)}', 24))
                location: location
                properties: {
                    sku: {
                        name: 'Basic'
                        family: 'C'
                        capacity: 1
                    }
                    enableNonSslPort: false
                    minimumTlsVersion: '1.2'
                }
                tags: {
                    'aspire-resource-name': 'cache'
                }
            }
 
            resource connectionString 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = {
                name: 'connectionString'
                properties: {
                    value: '${cache.properties.hostName},ssl=true,password=${cache.listKeys().primaryKey}'
                }
                parent: keyVault
            }
            """;
        output.WriteLine(manifest.BicepText);
        Assert.Equal(expectedBicep, manifest.BicepText);
    }
 
    [Fact]
    public async Task AddKeyVaultViaRunMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var mykv = builder.AddAzureKeyVault("mykv");
 
        var manifest = await ManifestUtils.GetManifestWithBicep(mykv.Resource);
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "{mykv.outputs.vaultUri}",
              "path": "mykv.module.bicep",
              "params": {
                "principalId": "",
                "principalType": ""
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "{mykv.outputs.vaultUri}",
              "path": "mykv.module.bicep",
              "params": {
                "principalId": "",
                "principalType": ""
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param principalId string
 
            param principalType string
 
            resource mykv 'Microsoft.KeyVault/vaults@2019-09-01' = {
                name: toLower(take('mykv${uniqueString(resourceGroup().id)}', 24))
                location: location
                properties: {
                    tenantId: tenant().tenantId
                    sku: {
                        family: 'A'
                        name: 'standard'
                    }
                    enableRbacAuthorization: true
                }
                tags: {
                    'aspire-resource-name': 'mykv'
                }
            }
 
            resource mykv_KeyVaultAdministrator 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(mykv.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483')
                    principalType: principalType
                }
                scope: mykv
            }
 
            output vaultUri string = mykv.properties.vaultUri
            """;
        output.WriteLine(manifest.BicepText);
        Assert.Equal(expectedBicep, manifest.BicepText);
    }
 
    [Fact]
    public async Task AddKeyVaultViaPublishMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var mykv = builder.AddAzureKeyVault("mykv");
 
        var manifest = await ManifestUtils.GetManifestWithBicep(mykv.Resource);
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "{mykv.outputs.vaultUri}",
              "path": "mykv.module.bicep",
              "params": {
                "principalId": "",
                "principalType": ""
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "{mykv.outputs.vaultUri}",
              "path": "mykv.module.bicep",
              "params": {
                "principalId": "",
                "principalType": ""
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param principalId string
 
            param principalType string
 
            resource mykv 'Microsoft.KeyVault/vaults@2019-09-01' = {
                name: toLower(take('mykv${uniqueString(resourceGroup().id)}', 24))
                location: location
                properties: {
                    tenantId: tenant().tenantId
                    sku: {
                        family: 'A'
                        name: 'standard'
                    }
                    enableRbacAuthorization: true
                }
                tags: {
                    'aspire-resource-name': 'mykv'
                }
            }
 
            resource mykv_KeyVaultAdministrator 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(mykv.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483')
                    principalType: principalType
                }
                scope: mykv
            }
 
            output vaultUri string = mykv.properties.vaultUri
            """;
        output.WriteLine(manifest.BicepText);
        Assert.Equal(expectedBicep, manifest.BicepText);
    }
 
    [Fact]
    public async Task AddAzureSignalR()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var signalr = builder.AddAzureSignalR("signalr");
 
        var manifest = await ManifestUtils.GetManifestWithBicep(signalr.Resource);
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "Endpoint=https://{signalr.outputs.hostName};AuthType=azure",
              "path": "signalr.module.bicep",
              "params": {
                "principalId": "",
                "principalType": ""
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "Endpoint=https://{signalr.outputs.hostName};AuthType=azure",
              "path": "signalr.module.bicep",
              "params": {
                "principalId": "",
                "principalType": ""
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param principalId string
 
            param principalType string
 
            resource signalr 'Microsoft.SignalRService/signalR@2022-02-01' = {
                name: toLower(take('signalr${uniqueString(resourceGroup().id)}', 24))
                location: location
                properties: {
                    cors: {
                        allowedOrigins: [
                            '*'
                        ]
                    }
                    features: [
                        {
                            flag: 'ServiceMode'
                            value: 'Default'
                        }
                    ]
                }
                kind: 'SignalR'
                sku: {
                    name: 'Free_F1'
                    capacity: 1
                }
                tags: {
                    'aspire-resource-name': 'signalr'
                }
            }
 
            resource signalr_SignalRAppServer 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(signalr.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '420fcaa2-552c-430f-98ca-3264be4806c7'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '420fcaa2-552c-430f-98ca-3264be4806c7')
                    principalType: principalType
                }
                scope: signalr
            }
 
            output hostName string = signalr.properties.hostName
            """;
        output.WriteLine(manifest.BicepText);
        Assert.Equal(expectedBicep, manifest.BicepText);
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task AsAzureSqlDatabaseViaRunMode(bool overrideDefaultTlsVersion)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var sql = builder.AddSqlServer("sql").AsAzureSqlDatabase((azureSqlBuilder, _, sql, _) =>
        {
            azureSqlBuilder.Resource.Outputs["sqlServerFqdn"] = "myserver";
 
            if (overrideDefaultTlsVersion)
            {
                sql.MinTlsVersion = SqlMinimalTlsVersion.Tls1_3;
            }
        });
        sql.AddDatabase("db", "dbName");
 
        var manifest = await ManifestUtils.GetManifestWithBicep(sql.Resource);
 
        Assert.Equal("Server=tcp:myserver,1433;Encrypt=True;Authentication=\"Active Directory Default\"", await sql.Resource.GetConnectionStringAsync(default));
        Assert.Equal("Server=tcp:{sql.outputs.sqlServerFqdn},1433;Encrypt=True;Authentication=\"Active Directory Default\"", sql.Resource.ConnectionStringExpression.ValueExpression);
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "Server=tcp:{sql.outputs.sqlServerFqdn},1433;Encrypt=True;Authentication=\u0022Active Directory Default\u0022",
              "path": "sql.module.bicep",
              "params": {
                "principalId": "",
                "principalName": "",
                "principalType": ""
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "Server=tcp:{sql.outputs.sqlServerFqdn},1433;Encrypt=True;Authentication=\u0022Active Directory Default\u0022",
              "path": "sql.module.bicep",
              "params": {
                "principalId": "",
                "principalName": "",
                "principalType": ""
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
 
        var expectedBicep = $$"""
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param principalId string
 
            param principalName string
 
            param principalType string
 
            resource sql 'Microsoft.Sql/servers@2021-11-01' = {
                name: toLower(take('sql${uniqueString(resourceGroup().id)}', 24))
                location: location
                properties: {
                    administrators: {
                        administratorType: 'ActiveDirectory'
                        principalType: principalType
                        login: principalName
                        sid: principalId
                        tenantId: subscription().tenantId
                        azureADOnlyAuthentication: true
                    }
                    minimalTlsVersion: '{{(overrideDefaultTlsVersion ? "1.3" : "1.2")}}'
                    publicNetworkAccess: 'Enabled'
                    version: '12.0'
                }
                tags: {
                    'aspire-resource-name': 'sql'
                }
            }
 
            resource sqlFirewallRule_AllowAllAzureIps 'Microsoft.Sql/servers/firewallRules@2021-11-01' = {
                name: 'AllowAllAzureIps'
                properties: {
                    endIpAddress: '0.0.0.0'
                    startIpAddress: '0.0.0.0'
                }
                parent: sql
            }
 
            resource sqlFirewallRule_AllowAllIps 'Microsoft.Sql/servers/firewallRules@2021-11-01' = {
                name: 'AllowAllIps'
                properties: {
                    endIpAddress: '255.255.255.255'
                    startIpAddress: '0.0.0.0'
                }
                parent: sql
            }
 
            resource db 'Microsoft.Sql/servers/databases@2021-11-01' = {
                name: 'dbName'
                location: location
                parent: sql
            }
 
            output sqlServerFqdn string = sql.properties.fullyQualifiedDomainName
            """;
        output.WriteLine(manifest.BicepText);
        Assert.Equal(expectedBicep, manifest.BicepText);
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task AsAzureSqlDatabaseViaPublishMode(bool overrideDefaultTlsVersion)
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var sql = builder.AddSqlServer("sql").AsAzureSqlDatabase((azureSqlBuilder, _, sql, _) =>
        {
            azureSqlBuilder.Resource.Outputs["sqlServerFqdn"] = "myserver";
 
            if (overrideDefaultTlsVersion)
            {
                sql.MinTlsVersion = SqlMinimalTlsVersion.Tls1_3;
            }
        });
        sql.AddDatabase("db", "dbName");
 
        var manifest = await ManifestUtils.GetManifestWithBicep(sql.Resource);
 
        Assert.Equal("Server=tcp:myserver,1433;Encrypt=True;Authentication=\"Active Directory Default\"", await sql.Resource.GetConnectionStringAsync(default));
        Assert.Equal("Server=tcp:{sql.outputs.sqlServerFqdn},1433;Encrypt=True;Authentication=\"Active Directory Default\"", sql.Resource.ConnectionStringExpression.ValueExpression);
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "Server=tcp:{sql.outputs.sqlServerFqdn},1433;Encrypt=True;Authentication=\u0022Active Directory Default\u0022",
              "path": "sql.module.bicep",
              "params": {
                "principalId": "",
                "principalName": ""
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "Server=tcp:{sql.outputs.sqlServerFqdn},1433;Encrypt=True;Authentication=\u0022Active Directory Default\u0022",
              "path": "sql.module.bicep",
              "params": {
                "principalId": "",
                "principalName": ""
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
 
        var expectedBicep = $$"""
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param principalId string
 
            param principalName string
 
            resource sql 'Microsoft.Sql/servers@2021-11-01' = {
                name: toLower(take('sql${uniqueString(resourceGroup().id)}', 24))
                location: location
                properties: {
                    administrators: {
                        administratorType: 'ActiveDirectory'
                        login: principalName
                        sid: principalId
                        tenantId: subscription().tenantId
                        azureADOnlyAuthentication: true
                    }
                    minimalTlsVersion: '{{(overrideDefaultTlsVersion ? "1.3" : "1.2")}}'
                    publicNetworkAccess: 'Enabled'
                    version: '12.0'
                }
                tags: {
                    'aspire-resource-name': 'sql'
                }
            }
 
            resource sqlFirewallRule_AllowAllAzureIps 'Microsoft.Sql/servers/firewallRules@2021-11-01' = {
                name: 'AllowAllAzureIps'
                properties: {
                    endIpAddress: '0.0.0.0'
                    startIpAddress: '0.0.0.0'
                }
                parent: sql
            }
 
            resource db 'Microsoft.Sql/servers/databases@2021-11-01' = {
                name: 'dbName'
                location: location
                parent: sql
            }
 
            output sqlServerFqdn string = sql.properties.fullyQualifiedDomainName
            """;
        output.WriteLine(manifest.BicepText);
        Assert.Equal(expectedBicep, manifest.BicepText);
    }
 
    [Fact]
    public async Task AsAzurePostgresFlexibleServerViaRunMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        builder.Configuration["Parameters:usr"] = "user";
        builder.Configuration["Parameters:pwd"] = "password";
 
        var usr = builder.AddParameter("usr");
        var pwd = builder.AddParameter("pwd", secret: true);
 
        IResourceBuilder<AzurePostgresResource>? azurePostgres = null;
        var postgres = builder.AddPostgres("postgres", usr, pwd).AsAzurePostgresFlexibleServer((resource, _, _) =>
        {
            Assert.NotNull(resource);
            azurePostgres = resource;
        });
        postgres.AddDatabase("db", "dbName");
 
        var manifest = await ManifestUtils.GetManifestWithBicep(postgres.Resource);
 
        // Setup to verify that connection strings is acquired via resource connectionstring redirct.
        Assert.NotNull(azurePostgres);
        azurePostgres.Resource.SecretOutputs["connectionString"] = "myconnectionstring";
        Assert.Equal("myconnectionstring", await postgres.Resource.GetConnectionStringAsync(default));
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "{postgres.secretOutputs.connectionString}",
              "path": "postgres.module.bicep",
              "params": {
                "keyVaultName": "",
                "administratorLogin": "{usr.value}",
                "administratorLoginPassword": "{pwd.value}"
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "{postgres.secretOutputs.connectionString}",
              "path": "postgres.module.bicep",
              "params": {
                "keyVaultName": "",
                "administratorLogin": "{usr.value}",
                "administratorLoginPassword": "{pwd.value}"
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param administratorLogin string
 
            @secure()
            param administratorLoginPassword string
 
            param keyVaultName string
 
            resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' existing = {
                name: keyVaultName
            }
 
            resource postgres 'Microsoft.DBforPostgreSQL/flexibleServers@2022-12-01' = {
                name: toLower(take('postgres${uniqueString(resourceGroup().id)}', 24))
                location: location
                properties: {
                    administratorLogin: administratorLogin
                    administratorLoginPassword: administratorLoginPassword
                    availabilityZone: '1'
                    backup: {
                        backupRetentionDays: 7
                        geoRedundantBackup: 'Disabled'
                    }
                    highAvailability: {
                        mode: 'Disabled'
                    }
                    storage: {
                        storageSizeGB: 32
                    }
                    version: '16'
                }
                sku: {
                    name: 'Standard_B1ms'
                    tier: 'Burstable'
                }
                tags: {
                    'aspire-resource-name': 'postgres'
                }
            }
 
            resource postgreSqlFirewallRule_AllowAllAzureIps 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2022-12-01' = {
                name: 'AllowAllAzureIps'
                properties: {
                    endIpAddress: '0.0.0.0'
                    startIpAddress: '0.0.0.0'
                }
                parent: postgres
            }
 
            resource postgreSqlFirewallRule_AllowAllIps 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2022-12-01' = {
                name: 'AllowAllIps'
                properties: {
                    endIpAddress: '255.255.255.255'
                    startIpAddress: '0.0.0.0'
                }
                parent: postgres
            }
 
            resource db 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2022-12-01' = {
                name: 'dbName'
                parent: postgres
            }
 
            resource connectionString 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = {
                name: 'connectionString'
                properties: {
                    value: 'Host=${postgres.properties.fullyQualifiedDomainName};Username=${administratorLogin};Password=${administratorLoginPassword}'
                }
                parent: keyVault
            }
            """;
        output.WriteLine(manifest.BicepText);
        Assert.Equal(expectedBicep, manifest.BicepText);
    }
 
    [Fact]
    public async Task AsAzurePostgresFlexibleServerViaPublishMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        builder.Configuration["Parameters:usr"] = "user";
        builder.Configuration["Parameters:pwd"] = "password";
 
        var usr = builder.AddParameter("usr");
        var pwd = builder.AddParameter("pwd", secret: true);
 
        IResourceBuilder<AzurePostgresResource>? azurePostgres = null;
        var postgres = builder.AddPostgres("postgres", usr, pwd).AsAzurePostgresFlexibleServer((resource, _, _) =>
        {
            Assert.NotNull(resource);
            azurePostgres = resource;
        });
        postgres.AddDatabase("db", "dbName");
 
        var manifest = await ManifestUtils.GetManifestWithBicep(postgres.Resource);
 
        // Setup to verify that connection strings is acquired via resource connectionstring redirct.
        Assert.NotNull(azurePostgres);
        azurePostgres.Resource.SecretOutputs["connectionString"] = "myconnectionstring";
        Assert.Equal("myconnectionstring", await postgres.Resource.GetConnectionStringAsync(default));
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "{postgres.secretOutputs.connectionString}",
              "path": "postgres.module.bicep",
              "params": {
                "keyVaultName": "",
                "administratorLogin": "{usr.value}",
                "administratorLoginPassword": "{pwd.value}"
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "{postgres.secretOutputs.connectionString}",
              "path": "postgres.module.bicep",
              "params": {
                "keyVaultName": "",
                "administratorLogin": "{usr.value}",
                "administratorLoginPassword": "{pwd.value}"
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param administratorLogin string
 
            @secure()
            param administratorLoginPassword string
 
            param keyVaultName string
 
            resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' existing = {
                name: keyVaultName
            }
 
            resource postgres 'Microsoft.DBforPostgreSQL/flexibleServers@2022-12-01' = {
                name: toLower(take('postgres${uniqueString(resourceGroup().id)}', 24))
                location: location
                properties: {
                    administratorLogin: administratorLogin
                    administratorLoginPassword: administratorLoginPassword
                    availabilityZone: '1'
                    backup: {
                        backupRetentionDays: 7
                        geoRedundantBackup: 'Disabled'
                    }
                    highAvailability: {
                        mode: 'Disabled'
                    }
                    storage: {
                        storageSizeGB: 32
                    }
                    version: '16'
                }
                sku: {
                    name: 'Standard_B1ms'
                    tier: 'Burstable'
                }
                tags: {
                    'aspire-resource-name': 'postgres'
                }
            }
 
            resource postgreSqlFirewallRule_AllowAllAzureIps 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2022-12-01' = {
                name: 'AllowAllAzureIps'
                properties: {
                    endIpAddress: '0.0.0.0'
                    startIpAddress: '0.0.0.0'
                }
                parent: postgres
            }
 
            resource db 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2022-12-01' = {
                name: 'dbName'
                parent: postgres
            }
 
            resource connectionString 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = {
                name: 'connectionString'
                properties: {
                    value: 'Host=${postgres.properties.fullyQualifiedDomainName};Username=${administratorLogin};Password=${administratorLoginPassword}'
                }
                parent: keyVault
            }
            """;
        output.WriteLine(manifest.BicepText);
        Assert.Equal(expectedBicep, manifest.BicepText);
    }
 
    [Fact]
    public async Task PublishAsAzurePostgresFlexibleServer()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        builder.Configuration["Parameters:usr"] = "user";
        builder.Configuration["Parameters:pwd"] = "password";
 
        var usr = builder.AddParameter("usr");
        var pwd = builder.AddParameter("pwd", secret: true);
 
        var postgres = builder.AddPostgres("postgres", usr, pwd).PublishAsAzurePostgresFlexibleServer();
        postgres.AddDatabase("db");
 
        var manifest = await ManifestUtils.GetManifestWithBicep(postgres.Resource);
 
        // Verify that when PublishAs variant is used, connection string acquisition
        // still uses the local endpoint.
        postgres.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 1234));
        var expectedConnectionString = $"Host=localhost;Port=1234;Username=user;Password=password";
        Assert.Equal(expectedConnectionString, await postgres.Resource.GetConnectionStringAsync());
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "{postgres.secretOutputs.connectionString}",
              "path": "postgres.module.bicep",
              "params": {
                "keyVaultName": "",
                "administratorLogin": "{usr.value}",
                "administratorLoginPassword": "{pwd.value}"
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "{postgres.secretOutputs.connectionString}",
              "path": "postgres.module.bicep",
              "params": {
                "keyVaultName": "",
                "administratorLogin": "{usr.value}",
                "administratorLoginPassword": "{pwd.value}"
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
    }
 
    [Fact]
    public async Task PublishAsAzurePostgresFlexibleServerNoUserPassParams()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var postgres = builder.AddPostgres("postgres1")
            .PublishAsAzurePostgresFlexibleServer(); // Because of InternalsVisibleTo
 
        var manifest = await ManifestUtils.GetManifest(postgres.Resource);
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "{postgres1.secretOutputs.connectionString}",
              "path": "postgres1.module.bicep",
              "params": {
                "keyVaultName": "",
                "administratorLogin": "{postgres1-username.value}",
                "administratorLoginPassword": "{postgres1-password.value}"
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "{postgres1.secretOutputs.connectionString}",
              "path": "postgres1.module.bicep",
              "params": {
                "keyVaultName": "",
                "administratorLogin": "{postgres1-username.value}",
                "administratorLoginPassword": "{postgres1-password.value}"
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ToString());
 
        var param = builder.AddParameter("param");
 
        postgres = builder.AddPostgres("postgres2", userName: param)
            .PublishAsAzurePostgresFlexibleServer();
 
        manifest = await ManifestUtils.GetManifest(postgres.Resource);
        expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "{postgres2.secretOutputs.connectionString}",
              "path": "postgres2.module.bicep",
              "params": {
                "keyVaultName": "",
                "administratorLogin": "{param.value}",
                "administratorLoginPassword": "{postgres2-password.value}"
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "{postgres2.secretOutputs.connectionString}",
              "path": "postgres2.module.bicep",
              "params": {
                "keyVaultName": "",
                "administratorLogin": "{param.value}",
                "administratorLoginPassword": "{postgres2-password.value}"
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ToString());
 
        postgres = builder.AddPostgres("postgres3", password: param)
            .PublishAsAzurePostgresFlexibleServer();
 
        manifest = await ManifestUtils.GetManifest(postgres.Resource);
        expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "{postgres3.secretOutputs.connectionString}",
              "path": "postgres3.module.bicep",
              "params": {
                "keyVaultName": "",
                "administratorLogin": "{postgres3-username.value}",
                "administratorLoginPassword": "{param.value}"
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "{postgres3.secretOutputs.connectionString}",
              "path": "postgres3.module.bicep",
              "params": {
                "keyVaultName": "",
                "administratorLogin": "{postgres3-username.value}",
                "administratorLoginPassword": "{param.value}"
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ToString());
    }
 
    [Fact]
    public async Task AddAzureServiceBus()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var serviceBus = builder.AddAzureServiceBus("sb");
 
        serviceBus
            .AddQueue("queue1")
            .AddQueue("queue2")
            .AddTopic("t1")
            .AddTopic("t2")
            .AddSubscription("t1", "s3");
 
        serviceBus.Resource.Outputs["serviceBusEndpoint"] = "mynamespaceEndpoint";
 
        var connectionStringResource = (IResourceWithConnectionString)serviceBus.Resource;
 
        Assert.Equal("sb", serviceBus.Resource.Name);
        Assert.Equal("mynamespaceEndpoint", await connectionStringResource.GetConnectionStringAsync());
        Assert.Equal("{sb.outputs.serviceBusEndpoint}", connectionStringResource.ConnectionStringExpression.ValueExpression);
 
        var manifest = await ManifestUtils.GetManifestWithBicep(serviceBus.Resource);
        var expected = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "{sb.outputs.serviceBusEndpoint}",
              "path": "sb.module.bicep",
              "params": {
                "principalId": "",
                "principalType": ""
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "{sb.outputs.serviceBusEndpoint}",
              "path": "sb.module.bicep",
              "params": {
                "principalId": "",
                "principalType": ""
              }
            }
            """;
        Assert.Equal(expected, manifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param sku string = 'Standard'
 
            param principalId string
 
            param principalType string
 
            resource sb 'Microsoft.ServiceBus/namespaces@2017-04-01' = {
                name: toLower(take('sb${uniqueString(resourceGroup().id)}', 24))
                location: location
                properties: {
                    disableLocalAuth: true
                }
                sku: {
                    name: sku
                }
                tags: {
                    'aspire-resource-name': 'sb'
                }
            }
 
            resource sb_AzureServiceBusDataOwner 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(sb.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '090c5cfd-751d-490a-894a-3ce6f1109419'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '090c5cfd-751d-490a-894a-3ce6f1109419')
                    principalType: principalType
                }
                scope: sb
            }
 
            resource queue1 'Microsoft.ServiceBus/namespaces/queues@2024-01-01' = {
                name: 'queue1'
                parent: sb
            }
 
            resource queue2 'Microsoft.ServiceBus/namespaces/queues@2024-01-01' = {
                name: 'queue2'
                parent: sb
            }
 
            resource t1 'Microsoft.ServiceBus/namespaces/topics@2024-01-01' = {
                name: 't1'
                parent: sb
            }
 
            resource t2 'Microsoft.ServiceBus/namespaces/topics@2024-01-01' = {
                name: 't2'
                parent: sb
            }
 
            resource s3 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2024-01-01' = {
                name: 's3'
                parent: t1
            }
 
            output serviceBusEndpoint string = sb.properties.serviceBusEndpoint
            """;
        output.WriteLine(manifest.BicepText);
        Assert.Equal(expectedBicep, manifest.BicepText);
    }
 
    [Fact]
    public async Task AddDefaultAzureWebPubSub()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var wps = builder.AddAzureWebPubSub("wps1");
 
        wps.Resource.Outputs["endpoint"] = "https://mywebpubsubendpoint";
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "{wps1.outputs.endpoint}",
              "path": "wps1.module.bicep",
              "params": {
                "principalId": "",
                "principalType": ""
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "{wps1.outputs.endpoint}",
              "path": "wps1.module.bicep",
              "params": {
                "principalId": "",
                "principalType": ""
              }
            }
            """;
 
        var connectionStringResource = (IResourceWithConnectionString)wps.Resource;
 
        Assert.Equal("https://mywebpubsubendpoint", await connectionStringResource.GetConnectionStringAsync());
        var manifest = await ManifestUtils.GetManifestWithBicep(wps.Resource);
        Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
 
        Assert.Equal("wps1", wps.Resource.Name);
        output.WriteLine(manifest.BicepText);
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param sku string = 'Free_F1'
 
            param capacity int = 1
 
            param principalId string
 
            param principalType string
 
            resource wps1 'Microsoft.SignalRService/webPubSub@2021-10-01' = {
                name: toLower(take('wps1${uniqueString(resourceGroup().id)}', 24))
                location: location
                sku: {
                    name: sku
                    capacity: capacity
                }
                tags: {
                    'aspire-resource-name': 'wps1'
                }
            }
 
            resource WebPubSubServiceOwner_wps1 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(resourceGroup().id, 'WebPubSubServiceOwner_wps1')
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '12cf5a90-567b-43ae-8102-96cf46c7d9b4')
                    principalType: principalType
                }
                scope: wps1
            }
 
            output endpoint string = 'https://${wps1.properties.hostName}'
            """;
        Assert.Equal(expectedBicep, manifest.BicepText);
    }
 
    [Fact]
    public async Task AddAzureWebPubSubWithParameters()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var wps = builder.AddAzureWebPubSub("wps1")
        .WithParameter("sku", "Standard_S1")
        .WithParameter("capacity", 2);
 
        wps.Resource.Outputs["endpoint"] = "https://mywebpubsubendpoint";
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "{wps1.outputs.endpoint}",
              "path": "wps1.module.bicep",
              "params": {
                "principalId": "",
                "principalType": "",
                "sku": "Standard_S1",
                "capacity": 2
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "{wps1.outputs.endpoint}",
              "path": "wps1.module.bicep",
              "params": {
                "principalId": "",
                "principalType": "",
                "sku": "Standard_S1",
                "capacity": 2
              }
            }
            """;
        var manifest = await ManifestUtils.GetManifestWithBicep(wps.Resource);
        Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
 
        Assert.Equal("wps1", wps.Resource.Name);
        output.WriteLine(manifest.BicepText);
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param sku string = 'Free_F1'
 
            param capacity int = 1
 
            param principalId string
 
            param principalType string
 
            resource wps1 'Microsoft.SignalRService/webPubSub@2021-10-01' = {
                name: toLower(take('wps1${uniqueString(resourceGroup().id)}', 24))
                location: location
                sku: {
                    name: sku
                    capacity: capacity
                }
                tags: {
                    'aspire-resource-name': 'wps1'
                }
            }
 
            resource WebPubSubServiceOwner_wps1 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(resourceGroup().id, 'WebPubSubServiceOwner_wps1')
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '12cf5a90-567b-43ae-8102-96cf46c7d9b4')
                    principalType: principalType
                }
                scope: wps1
            }
 
            output endpoint string = 'https://${wps1.properties.hostName}'
            """;
        Assert.Equal(expectedBicep, manifest.BicepText);
    }
 
    [Fact]
    public async Task AddAzureStorageEmulator()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var storage = builder.AddAzureStorage("storage").RunAsEmulator(e =>
        {
            e.WithEndpoint("blob", e => e.AllocatedEndpoint = new(e, "localhost", 10000));
            e.WithEndpoint("queue", e => e.AllocatedEndpoint = new(e, "localhost", 10001));
            e.WithEndpoint("table", e => e.AllocatedEndpoint = new(e, "localhost", 10002));
        });
 
        Assert.True(storage.Resource.IsContainer());
 
        var blob = storage.AddBlobs("blob");
        var queue = storage.AddQueues("queue");
        var table = storage.AddTables("table");
 
        var blobqs = AzureStorageEmulatorConnectionString.Create(blobPort: 10000);
        var queueqs = AzureStorageEmulatorConnectionString.Create(queuePort: 10001);
        var tableqs = AzureStorageEmulatorConnectionString.Create(tablePort: 10002);
 
        Assert.Equal(blobqs, blob.Resource.ConnectionStringExpression.ValueExpression);
        Assert.Equal(queueqs, queue.Resource.ConnectionStringExpression.ValueExpression);
        Assert.Equal(tableqs, table.Resource.ConnectionStringExpression.ValueExpression);
 
        Assert.Equal(blobqs, await ((IResourceWithConnectionString)blob.Resource).GetConnectionStringAsync());
        Assert.Equal(queueqs, await ((IResourceWithConnectionString)queue.Resource).GetConnectionStringAsync());
        Assert.Equal(tableqs, await ((IResourceWithConnectionString)table.Resource).GetConnectionStringAsync());
    }
 
    [Fact]
    public async Task AddAzureStorageViaRunMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var storagesku = builder.AddParameter("storagesku");
        var storage = builder.AddAzureStorage("storage", (_, construct, sa) =>
        {
            sa.Sku = new StorageSku()
            {
                Name = storagesku.AsBicepParameter(construct)
            };
        });
 
        storage.Resource.Outputs["blobEndpoint"] = "https://myblob";
        storage.Resource.Outputs["queueEndpoint"] = "https://myqueue";
        storage.Resource.Outputs["tableEndpoint"] = "https://mytable";
 
        // Check storage resource.
        Assert.Equal("storage", storage.Resource.Name);
 
        var storageManifest = await ManifestUtils.GetManifestWithBicep(storage.Resource);
 
        var expectedStorageManifest = """
            {
              "type": "azure.bicep.v0",
              "path": "storage.module.bicep",
              "params": {
                "principalId": "",
                "principalType": "",
                "storagesku": "{storagesku.value}"
              }
            }
            """{
              "type": "azure.bicep.v0",
              "path": "storage.module.bicep",
              "params": {
                "principalId": "",
                "principalType": "",
                "storagesku": "{storagesku.value}"
              }
            }
            """;
        Assert.Equal(expectedStorageManifest, storageManifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param storagesku string
 
            param principalId string
 
            param principalType string
 
            resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
                name: toLower(take('storage${uniqueString(resourceGroup().id)}', 24))
                kind: 'StorageV2'
                location: location
                sku: {
                    name: storagesku
                }
                properties: {
                    accessTier: 'Hot'
                    allowSharedKeyAccess: false
                    minimumTlsVersion: 'TLS1_2'
                    networkAcls: {
                        defaultAction: 'Allow'
                    }
                }
                tags: {
                    'aspire-resource-name': 'storage'
                }
            }
 
            resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
                name: 'default'
                parent: storage
            }
 
            resource storage_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(storage.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
                    principalType: principalType
                }
                scope: storage
            }
 
            resource storage_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(storage.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')
                    principalType: principalType
                }
                scope: storage
            }
 
            resource storage_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(storage.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
                    principalType: principalType
                }
                scope: storage
            }
 
            output blobEndpoint string = storage.properties.primaryEndpoints.blob
 
            output queueEndpoint string = storage.properties.primaryEndpoints.queue
 
            output tableEndpoint string = storage.properties.primaryEndpoints.table
            """;
        output.WriteLine(storageManifest.BicepText);
        Assert.Equal(expectedBicep, storageManifest.BicepText);
 
        // Check blob resource.
        var blob = storage.AddBlobs("blob");
 
        var connectionStringBlobResource = (IResourceWithConnectionString)blob.Resource;
 
        Assert.Equal("https://myblob", await connectionStringBlobResource.GetConnectionStringAsync());
        var expectedBlobManifest = """
            {
              "type": "value.v0",
              "connectionString": "{storage.outputs.blobEndpoint}"
            }
            """{
              "type": "value.v0",
              "connectionString": "{storage.outputs.blobEndpoint}"
            }
            """;
        var blobManifest = await ManifestUtils.GetManifest(blob.Resource);
        Assert.Equal(expectedBlobManifest, blobManifest.ToString());
 
        // Check queue resource.
        var queue = storage.AddQueues("queue");
 
        var connectionStringQueueResource = (IResourceWithConnectionString)queue.Resource;
 
        Assert.Equal("https://myqueue", await connectionStringQueueResource.GetConnectionStringAsync());
        var expectedQueueManifest = """
            {
              "type": "value.v0",
              "connectionString": "{storage.outputs.queueEndpoint}"
            }
            """{
              "type": "value.v0",
              "connectionString": "{storage.outputs.queueEndpoint}"
            }
            """;
        var queueManifest = await ManifestUtils.GetManifest(queue.Resource);
        Assert.Equal(expectedQueueManifest, queueManifest.ToString());
 
        // Check table resource.
        var table = storage.AddTables("table");
 
        var connectionStringTableResource = (IResourceWithConnectionString)table.Resource;
 
        Assert.Equal("https://mytable", await connectionStringTableResource.GetConnectionStringAsync());
        var expectedTableManifest = """
            {
              "type": "value.v0",
              "connectionString": "{storage.outputs.tableEndpoint}"
            }
            """{
              "type": "value.v0",
              "connectionString": "{storage.outputs.tableEndpoint}"
            }
            """;
        var tableManifest = await ManifestUtils.GetManifest(table.Resource);
        Assert.Equal(expectedTableManifest, tableManifest.ToString());
    }
 
    [Fact]
    public async Task AddAzureStorageViaRunModeAllowSharedKeyAccessOverridesDefaultFalse()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var storagesku = builder.AddParameter("storagesku");
        var storage = builder.AddAzureStorage("storage", (_, construct, sa) =>
        {
            sa.Sku = new StorageSku()
            {
                Name = storagesku.AsBicepParameter(construct)
            };
            sa.AllowSharedKeyAccess = true;
        });
 
        storage.Resource.Outputs["blobEndpoint"] = "https://myblob";
        storage.Resource.Outputs["queueEndpoint"] = "https://myqueue";
        storage.Resource.Outputs["tableEndpoint"] = "https://mytable";
 
        // Check storage resource.
        Assert.Equal("storage", storage.Resource.Name);
 
        var storageManifest = await ManifestUtils.GetManifestWithBicep(storage.Resource);
 
        var expectedStorageManifest = """
            {
              "type": "azure.bicep.v0",
              "path": "storage.module.bicep",
              "params": {
                "principalId": "",
                "principalType": "",
                "storagesku": "{storagesku.value}"
              }
            }
            """{
              "type": "azure.bicep.v0",
              "path": "storage.module.bicep",
              "params": {
                "principalId": "",
                "principalType": "",
                "storagesku": "{storagesku.value}"
              }
            }
            """;
        Assert.Equal(expectedStorageManifest, storageManifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param storagesku string
 
            param principalId string
 
            param principalType string
 
            resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
                name: toLower(take('storage${uniqueString(resourceGroup().id)}', 24))
                kind: 'StorageV2'
                location: location
                sku: {
                    name: storagesku
                }
                properties: {
                    accessTier: 'Hot'
                    allowSharedKeyAccess: true
                    minimumTlsVersion: 'TLS1_2'
                    networkAcls: {
                        defaultAction: 'Allow'
                    }
                }
                tags: {
                    'aspire-resource-name': 'storage'
                }
            }
 
            resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
                name: 'default'
                parent: storage
            }
 
            resource storage_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(storage.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
                    principalType: principalType
                }
                scope: storage
            }
 
            resource storage_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(storage.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')
                    principalType: principalType
                }
                scope: storage
            }
 
            resource storage_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(storage.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
                    principalType: principalType
                }
                scope: storage
            }
 
            output blobEndpoint string = storage.properties.primaryEndpoints.blob
 
            output queueEndpoint string = storage.properties.primaryEndpoints.queue
 
            output tableEndpoint string = storage.properties.primaryEndpoints.table
            """;
        output.WriteLine(storageManifest.BicepText);
        Assert.Equal(expectedBicep, storageManifest.BicepText);
 
        // Check blob resource.
        var blob = storage.AddBlobs("blob");
 
        var connectionStringBlobResource = (IResourceWithConnectionString)blob.Resource;
 
        Assert.Equal("https://myblob", await connectionStringBlobResource.GetConnectionStringAsync());
        var expectedBlobManifest = """
            {
              "type": "value.v0",
              "connectionString": "{storage.outputs.blobEndpoint}"
            }
            """{
              "type": "value.v0",
              "connectionString": "{storage.outputs.blobEndpoint}"
            }
            """;
        var blobManifest = await ManifestUtils.GetManifest(blob.Resource);
        Assert.Equal(expectedBlobManifest, blobManifest.ToString());
 
        // Check queue resource.
        var queue = storage.AddQueues("queue");
 
        var connectionStringQueueResource = (IResourceWithConnectionString)queue.Resource;
 
        Assert.Equal("https://myqueue", await connectionStringQueueResource.GetConnectionStringAsync());
        var expectedQueueManifest = """
            {
              "type": "value.v0",
              "connectionString": "{storage.outputs.queueEndpoint}"
            }
            """{
              "type": "value.v0",
              "connectionString": "{storage.outputs.queueEndpoint}"
            }
            """;
        var queueManifest = await ManifestUtils.GetManifest(queue.Resource);
        Assert.Equal(expectedQueueManifest, queueManifest.ToString());
 
        // Check table resource.
        var table = storage.AddTables("table");
 
        var connectionStringTableResource = (IResourceWithConnectionString)table.Resource;
 
        Assert.Equal("https://mytable", await connectionStringTableResource.GetConnectionStringAsync());
        var expectedTableManifest = """
            {
              "type": "value.v0",
              "connectionString": "{storage.outputs.tableEndpoint}"
            }
            """{
              "type": "value.v0",
              "connectionString": "{storage.outputs.tableEndpoint}"
            }
            """;
        var tableManifest = await ManifestUtils.GetManifest(table.Resource);
        Assert.Equal(expectedTableManifest, tableManifest.ToString());
    }
 
    [Fact]
    public async Task AddAzureStorageViaPublishMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var storagesku = builder.AddParameter("storagesku");
        var storage = builder.AddAzureStorage("storage", (_, construct, sa) =>
        {
            sa.Sku = new StorageSku()
            {
                Name = storagesku.AsBicepParameter(construct)
            };
        });
 
        storage.Resource.Outputs["blobEndpoint"] = "https://myblob";
        storage.Resource.Outputs["queueEndpoint"] = "https://myqueue";
        storage.Resource.Outputs["tableEndpoint"] = "https://mytable";
 
        // Check storage resource.
        Assert.Equal("storage", storage.Resource.Name);
 
        var storageManifest = await ManifestUtils.GetManifestWithBicep(storage.Resource);
 
        var expectedStorageManifest = """
            {
              "type": "azure.bicep.v0",
              "path": "storage.module.bicep",
              "params": {
                "principalId": "",
                "principalType": "",
                "storagesku": "{storagesku.value}"
              }
            }
            """{
              "type": "azure.bicep.v0",
              "path": "storage.module.bicep",
              "params": {
                "principalId": "",
                "principalType": "",
                "storagesku": "{storagesku.value}"
              }
            }
            """;
        Assert.Equal(expectedStorageManifest, storageManifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param storagesku string
 
            param principalId string
 
            param principalType string
 
            resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
                name: toLower(take('storage${uniqueString(resourceGroup().id)}', 24))
                kind: 'StorageV2'
                location: location
                sku: {
                    name: storagesku
                }
                properties: {
                    accessTier: 'Hot'
                    allowSharedKeyAccess: false
                    minimumTlsVersion: 'TLS1_2'
                    networkAcls: {
                        defaultAction: 'Allow'
                    }
                }
                tags: {
                    'aspire-resource-name': 'storage'
                }
            }
 
            resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
                name: 'default'
                parent: storage
            }
 
            resource storage_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(storage.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
                    principalType: principalType
                }
                scope: storage
            }
 
            resource storage_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(storage.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')
                    principalType: principalType
                }
                scope: storage
            }
 
            resource storage_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(storage.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
                    principalType: principalType
                }
                scope: storage
            }
 
            output blobEndpoint string = storage.properties.primaryEndpoints.blob
 
            output queueEndpoint string = storage.properties.primaryEndpoints.queue
 
            output tableEndpoint string = storage.properties.primaryEndpoints.table
            """;
        output.WriteLine(storageManifest.BicepText);
        Assert.Equal(expectedBicep, storageManifest.BicepText);
 
        // Check blob resource.
        var blob = storage.AddBlobs("blob");
 
        var connectionStringBlobResource = (IResourceWithConnectionString)blob.Resource;
 
        Assert.Equal("https://myblob", await connectionStringBlobResource.GetConnectionStringAsync());
        var expectedBlobManifest = """
            {
              "type": "value.v0",
              "connectionString": "{storage.outputs.blobEndpoint}"
            }
            """{
              "type": "value.v0",
              "connectionString": "{storage.outputs.blobEndpoint}"
            }
            """;
        var blobManifest = await ManifestUtils.GetManifest(blob.Resource);
        Assert.Equal(expectedBlobManifest, blobManifest.ToString());
 
        // Check queue resource.
        var queue = storage.AddQueues("queue");
 
        var connectionStringQueueResource = (IResourceWithConnectionString)queue.Resource;
 
        Assert.Equal("https://myqueue", await connectionStringQueueResource.GetConnectionStringAsync());
        var expectedQueueManifest = """
            {
              "type": "value.v0",
              "connectionString": "{storage.outputs.queueEndpoint}"
            }
            """{
              "type": "value.v0",
              "connectionString": "{storage.outputs.queueEndpoint}"
            }
            """;
        var queueManifest = await ManifestUtils.GetManifest(queue.Resource);
        Assert.Equal(expectedQueueManifest, queueManifest.ToString());
 
        // Check table resource.
        var table = storage.AddTables("table");
 
        var connectionStringTableResource = (IResourceWithConnectionString)table.Resource;
 
        Assert.Equal("https://mytable", await connectionStringTableResource.GetConnectionStringAsync());
        var expectedTableManifest = """
            {
              "type": "value.v0",
              "connectionString": "{storage.outputs.tableEndpoint}"
            }
            """{
              "type": "value.v0",
              "connectionString": "{storage.outputs.tableEndpoint}"
            }
            """;
        var tableManifest = await ManifestUtils.GetManifest(table.Resource);
        Assert.Equal(expectedTableManifest, tableManifest.ToString());
    }
 
    [Fact]
    public async Task AddAzureStorageViaPublishModeEnableAllowSharedKeyAccessOverridesDefaultFalse()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var storagesku = builder.AddParameter("storagesku");
        var storage = builder.AddAzureStorage("storage", (_, construct, sa) =>
        {
            sa.Sku = new StorageSku()
            {
                Name = storagesku.AsBicepParameter(construct)
            };
            sa.AllowSharedKeyAccess = true;
        });
 
        storage.Resource.Outputs["blobEndpoint"] = "https://myblob";
        storage.Resource.Outputs["queueEndpoint"] = "https://myqueue";
        storage.Resource.Outputs["tableEndpoint"] = "https://mytable";
 
        // Check storage resource.
        Assert.Equal("storage", storage.Resource.Name);
 
        var storageManifest = await ManifestUtils.GetManifestWithBicep(storage.Resource);
 
        var expectedStorageManifest = """
            {
              "type": "azure.bicep.v0",
              "path": "storage.module.bicep",
              "params": {
                "principalId": "",
                "principalType": "",
                "storagesku": "{storagesku.value}"
              }
            }
            """{
              "type": "azure.bicep.v0",
              "path": "storage.module.bicep",
              "params": {
                "principalId": "",
                "principalType": "",
                "storagesku": "{storagesku.value}"
              }
            }
            """;
        Assert.Equal(expectedStorageManifest, storageManifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param storagesku string
 
            param principalId string
 
            param principalType string
 
            resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
                name: toLower(take('storage${uniqueString(resourceGroup().id)}', 24))
                kind: 'StorageV2'
                location: location
                sku: {
                    name: storagesku
                }
                properties: {
                    accessTier: 'Hot'
                    allowSharedKeyAccess: true
                    minimumTlsVersion: 'TLS1_2'
                    networkAcls: {
                        defaultAction: 'Allow'
                    }
                }
                tags: {
                    'aspire-resource-name': 'storage'
                }
            }
 
            resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
                name: 'default'
                parent: storage
            }
 
            resource storage_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(storage.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
                    principalType: principalType
                }
                scope: storage
            }
 
            resource storage_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(storage.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')
                    principalType: principalType
                }
                scope: storage
            }
 
            resource storage_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(storage.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
                    principalType: principalType
                }
                scope: storage
            }
 
            output blobEndpoint string = storage.properties.primaryEndpoints.blob
 
            output queueEndpoint string = storage.properties.primaryEndpoints.queue
 
            output tableEndpoint string = storage.properties.primaryEndpoints.table
            """;
        output.WriteLine(storageManifest.BicepText);
        Assert.Equal(expectedBicep, storageManifest.BicepText);
 
        // Check blob resource.
        var blob = storage.AddBlobs("blob");
 
        var connectionStringBlobResource = (IResourceWithConnectionString)blob.Resource;
 
        Assert.Equal("https://myblob", await connectionStringBlobResource.GetConnectionStringAsync());
        var expectedBlobManifest = """
            {
              "type": "value.v0",
              "connectionString": "{storage.outputs.blobEndpoint}"
            }
            """{
              "type": "value.v0",
              "connectionString": "{storage.outputs.blobEndpoint}"
            }
            """;
        var blobManifest = await ManifestUtils.GetManifest(blob.Resource);
        Assert.Equal(expectedBlobManifest, blobManifest.ToString());
 
        // Check queue resource.
        var queue = storage.AddQueues("queue");
 
        var connectionStringQueueResource = (IResourceWithConnectionString)queue.Resource;
 
        Assert.Equal("https://myqueue", await connectionStringQueueResource.GetConnectionStringAsync());
        var expectedQueueManifest = """
            {
              "type": "value.v0",
              "connectionString": "{storage.outputs.queueEndpoint}"
            }
            """{
              "type": "value.v0",
              "connectionString": "{storage.outputs.queueEndpoint}"
            }
            """;
        var queueManifest = await ManifestUtils.GetManifest(queue.Resource);
        Assert.Equal(expectedQueueManifest, queueManifest.ToString());
 
        // Check table resource.
        var table = storage.AddTables("table");
 
        var connectionStringTableResource = (IResourceWithConnectionString)table.Resource;
 
        Assert.Equal("https://mytable", await connectionStringTableResource.GetConnectionStringAsync());
        var expectedTableManifest = """
            {
              "type": "value.v0",
              "connectionString": "{storage.outputs.tableEndpoint}"
            }
            """{
              "type": "value.v0",
              "connectionString": "{storage.outputs.tableEndpoint}"
            }
            """;
        var tableManifest = await ManifestUtils.GetManifest(table.Resource);
        Assert.Equal(expectedTableManifest, tableManifest.ToString());
    }
 
    [Fact]
    public async Task AddAzureSearch()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        // Add search and parameterize the SKU
        var sku = builder.AddParameter("searchSku");
        var search = builder.AddAzureSearch("search", (_, construct, search) =>
            search.SearchSkuName = sku.AsBicepParameter(construct));
 
        // Pretend we deployed it
        const string fakeConnectionString = "mysearchconnectionstring";
        search.Resource.Outputs["connectionString"] = fakeConnectionString;
 
        var connectionStringResource = (IResourceWithConnectionString)search.Resource;
 
        // Validate the resource
        Assert.Equal("search", search.Resource.Name);
        Assert.Equal("{search.outputs.connectionString}", connectionStringResource.ConnectionStringExpression.ValueExpression);
        Assert.Equal(fakeConnectionString, await connectionStringResource.GetConnectionStringAsync());
 
        var manifest = await ManifestUtils.GetManifestWithBicep(search.Resource);
 
        // Validate the manifest
        var expectedManifest =
            """
            {
              "type": "azure.bicep.v0",
              "connectionString": "{search.outputs.connectionString}",
              "path": "search.module.bicep",
              "params": {
                "principalId": "",
                "principalType": "",
                "searchSku": "{searchSku.value}"
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "{search.outputs.connectionString}",
              "path": "search.module.bicep",
              "params": {
                "principalId": "",
                "principalType": "",
                "searchSku": "{searchSku.value}"
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param searchSku string
 
            param principalId string
 
            param principalType string
 
            resource search 'Microsoft.Search/searchServices@2023-11-01' = {
                name: toLower(take('search${uniqueString(resourceGroup().id)}', 24))
                location: location
                properties: {
                    hostingMode: 'default'
                    disableLocalAuth: true
                    partitionCount: 1
                    replicaCount: 1
                }
                sku: {
                    name: searchSku
                }
                tags: {
                    'aspire-resource-name': 'search'
                }
            }
 
            resource search_SearchIndexDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(search.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7')
                    principalType: principalType
                }
                scope: search
            }
 
            resource search_SearchServiceContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(search.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0')
                    principalType: principalType
                }
                scope: search
            }
 
            output connectionString string = 'Endpoint=https://${search.name}.search.windows.net'
            """;
        output.WriteLine(manifest.BicepText);
        Assert.Equal(expectedBicep, manifest.BicepText);
    }
 
    [Fact]
    public async Task PublishAsConnectionString()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var ai = builder.AddAzureApplicationInsights("ai").PublishAsConnectionString();
        var serviceBus = builder.AddAzureServiceBus("servicebus").PublishAsConnectionString();
 
        var serviceA = builder.AddProject<ProjectA>("serviceA", o => o.ExcludeLaunchProfile = true)
            .WithReference(ai)
            .WithReference(serviceBus);
 
        var aiManifest = await ManifestUtils.GetManifest(ai.Resource);
        Assert.Equal("{ai.value}", aiManifest["connectionString"]?.ToString());
        Assert.Equal("parameter.v0", aiManifest["type"]?.ToString());
 
        var serviceBusManifest = await ManifestUtils.GetManifest(serviceBus.Resource);
        Assert.Equal("{servicebus.value}", serviceBusManifest["connectionString"]?.ToString());
        Assert.Equal("parameter.v0", serviceBusManifest["type"]?.ToString());
 
        var serviceManifest = await ManifestUtils.GetManifest(serviceA.Resource);
        Assert.Equal("{ai.connectionString}", serviceManifest["env"]?["APPLICATIONINSIGHTS_CONNECTION_STRING"]?.ToString());
        Assert.Equal("{servicebus.connectionString}", serviceManifest["env"]?["ConnectionStrings__servicebus"]?.ToString());
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task AddAzureOpenAI(bool overrideLocalAuthDefault)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        IEnumerable<CognitiveServicesAccountDeployment>? aiDeployments = null;
        var openai = builder.AddAzureOpenAI("openai", (_, _, account, deployments) =>
        {
            aiDeployments = deployments;
 
            if (overrideLocalAuthDefault)
            {
                account.Properties.Value!.DisableLocalAuth = false;
            }
        })
            .AddDeployment(new("mymodel", "gpt-35-turbo", "0613", "Basic", 4))
            .AddDeployment(new("embedding-model", "text-embedding-ada-002", "2", "Basic", 4));
 
        var manifest = await ManifestUtils.GetManifestWithBicep(openai.Resource);
 
        Assert.NotNull(aiDeployments);
        Assert.Collection(
            aiDeployments,
            deployment => Assert.Equal("mymodel", deployment.Name.Value),
            deployment => Assert.Equal("embedding-model", deployment.Name.Value));
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "connectionString": "{openai.outputs.connectionString}",
              "path": "openai.module.bicep",
              "params": {
                "principalId": "",
                "principalType": ""
              }
            }
            """{
              "type": "azure.bicep.v0",
              "connectionString": "{openai.outputs.connectionString}",
              "path": "openai.module.bicep",
              "params": {
                "principalId": "",
                "principalType": ""
              }
            }
            """;
        Assert.Equal(expectedManifest, manifest.ManifestNode.ToString());
 
        var expectedBicep = $$"""
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param principalId string
 
            param principalType string
 
            resource openai 'Microsoft.CognitiveServices/accounts@2022-12-01' = {
                name: toLower(take('openai${uniqueString(resourceGroup().id)}', 24))
                location: location
                kind: 'OpenAI'
                properties: {
                    customSubDomainName: toLower(take(concat('openai', uniqueString(resourceGroup().id)), 24))
                    publicNetworkAccess: 'Enabled'
                    disableLocalAuth: {{(overrideLocalAuthDefault ? "false" : "true")}}
                }
                sku: {
                    name: 'S0'
                }
                tags: {
                    'aspire-resource-name': 'openai'
                }
            }
 
            resource openai_CognitiveServicesOpenAIContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
                name: guid(openai.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442'))
                properties: {
                    principalId: principalId
                    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442')
                    principalType: principalType
                }
                scope: openai
            }
 
            resource mymodel 'Microsoft.CognitiveServices/accounts/deployments@2022-12-01' = {
                name: 'mymodel'
                properties: {
                    model: {
                        format: 'OpenAI'
                        name: 'gpt-35-turbo'
                        version: '0613'
                    }
                }
                sku: {
                    name: 'Basic'
                    capacity: 4
                }
                parent: openai
            }
 
            resource embedding-model 'Microsoft.CognitiveServices/accounts/deployments@2022-12-01' = {
                name: 'embedding-model'
                properties: {
                    model: {
                        format: 'OpenAI'
                        name: 'text-embedding-ada-002'
                        version: '2'
                    }
                }
                sku: {
                    name: 'Basic'
                    capacity: 4
                }
                parent: openai
                dependsOn: [
                    mymodel
                ]
            }
 
            output connectionString string = 'Endpoint=${openai.properties.endpoint}'
            """;
        output.WriteLine(manifest.BicepText);
        Assert.Equal(expectedBicep, manifest.BicepText);
    }
 
    [Fact]
    public void ConfigureConstructMustNotBeNull()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var constructResource = builder.AddAzureConstruct("construct", r =>
        {
            r.Add(new KeyVaultService("kv"));
        });
 
        var ex = Assert.Throws<ArgumentNullException>(() => constructResource.ConfigureConstruct(null!));
        Assert.Equal("configure", ex.ParamName);
    }
 
    [Fact]
    public async Task ConstructCanBeMutatedAfterCreation()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var constructResource = builder.AddAzureConstruct("construct", r =>
        {
            r.Add(new KeyVaultService("kv")
            {
                Properties = new KeyVaultProperties()
                {
                    TenantId = BicepFunction.GetTenant().TenantId,
                    Sku = new KeyVaultSku()
                    {
                        Family = KeyVaultSkuFamily.A,
                        Name = KeyVaultSkuName.Standard
                    },
                    EnableRbacAuthorization = true
                }
            });
        })
        .ConfigureConstruct(r =>
        {
            var vault = r.GetResources().OfType<KeyVaultService>().Single();
            Assert.NotNull(vault);
 
            r.Add(new BicepOutput("vaultUri", typeof(string))
            {
                Value =
                    new MemberExpression(
                        new MemberExpression(
                            new IdentifierExpression(vault.ResourceName),
                            "properties"),
                        "vaultUri")
                // TODO: this should be
                //Value = keyVault.VaultUri
            });
        })
        .ConfigureConstruct(r =>
        {
            var vault = r.GetResources().OfType<KeyVaultService>().Single();
            Assert.NotNull(vault);
 
            r.Add(new KeyVaultSecret("secret")
            {
                Parent = vault,
                Name = "kvs",
                Properties = new SecretProperties { Value = "00000000-0000-0000-0000-000000000000" }
            });
        });
 
        var (manifest, bicep) = await ManifestUtils.GetManifestWithBicep(constructResource.Resource);
 
        var expectedManifest = """
            {
              "type": "azure.bicep.v0",
              "path": "construct.module.bicep"
            }
            """{
              "type": "azure.bicep.v0",
              "path": "construct.module.bicep"
            }
            """;
 
        var expectedBicep = """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            resource kv 'Microsoft.KeyVault/vaults@2019-09-01' = {
                name: toLower(take('kv${uniqueString(resourceGroup().id)}', 24))
                location: location
                properties: {
                    tenantId: tenant().tenantId
                    sku: {
                        family: 'A'
                        name: 'standard'
                    }
                    enableRbacAuthorization: true
                }
            }
 
            resource secret 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = {
                name: 'kvs'
                properties: {
                    value: '00000000-0000-0000-0000-000000000000'
                }
                parent: kv
            }
 
            output vaultUri string = kv.properties.vaultUri
            """;
 
        Assert.Equal(expectedManifest, manifest.ToString());
        Assert.Equal(expectedBicep, bicep);
    }
 
    private sealed class ProjectA : IProjectMetadata
    {
        public string ProjectPath => "projectA";
    }
}