File: AzureKeyVaultTests.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.
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
using Azure.Provisioning;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting.Azure.Tests;
 
public class AzureKeyVaultTests
{
    [Fact]
    public async Task AddKeyVaultViaRunMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var mykv = builder.AddAzureKeyVault("mykv");
 
        var (manifest, bicep) = await AzureManifestUtils.GetManifestWithBicep(mykv.Resource);
 
        await Verify(manifest.ToString(), "json")
              .AppendContentAsFile(bicep, "bicep");
              
    }
 
    [Fact]
    public async Task AddKeyVaultViaPublishMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var mykv = builder.AddAzureKeyVault("mykv");
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var (manifest, bicep) = await AzureManifestUtils.GetManifestWithBicep(model, mykv.Resource);
        var kvRoles = Assert.Single(model.Resources.OfType<AzureProvisioningResource>(), r => r.Name == "mykv-roles");
        var (kvRolesManifest, kvRolesBicep) = await AzureManifestUtils.GetManifestWithBicep(kvRoles, skipPreparer: true);
 
        await Verify(manifest.ToString(), "json")
              .AppendContentAsFile(bicep, "bicep")
              .AppendContentAsFile(kvRolesBicep, "bicep")
              .AppendContentAsFile(kvRolesManifest.ToString(), "json");
              
    }
 
    [Fact]
    public async Task WithEnvironment_AddsKeyVaultSecretReference()
    {
        // Arrange: Create a test application builder.
        using var builder = TestDistributedApplicationBuilder.Create();
 
        // Add a key vault resource.
        var kv = builder.AddAzureKeyVault("myKeyVault");
 
        kv.Resource.SecretResolver = (s, ct) =>
        {
            return Task.FromResult<string?>("my secret value");
        };
 
        // Get a secret reference from the key vault resource.
        var secretReference = kv.Resource.GetSecret("mySecret");
 
        // Add a container resource that supports environment variables.
        var containerBuilder = builder.AddContainer("myContainer", "nginx")
                                       .WithEnvironment("MY_SECRET", secretReference);
 
        var runEnv = await containerBuilder.Resource.GetEnvironmentVariableValuesAsync(DistributedApplicationOperation.Run);
        var publishEnv = await containerBuilder.Resource.GetEnvironmentVariableValuesAsync(DistributedApplicationOperation.Publish);
 
        var runKvp = Assert.Single(runEnv);
        var pubishKvp = Assert.Single(publishEnv);
 
        Assert.Equal("MY_SECRET", runKvp.Key);
        Assert.Same("my secret value", runKvp.Value);
 
        Assert.Equal("MY_SECRET", pubishKvp.Key);
        Assert.Equal("{myKeyVault.secrets.mySecret}", pubishKvp.Value);
    }
 
    [Fact]
    public async Task ConsumingAKeyVaultSecretInAnotherBicepModule()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var kv = builder.AddAzureKeyVault("myKeyVault");
 
        var secretReference = kv.Resource.GetSecret("mySecret");
        var secretReference2 = kv.Resource.GetSecret("mySecret2");
 
        var module = builder.AddAzureInfrastructure("mymodule", infra =>
        {
            var secret = secretReference.AsKeyVaultSecret(infra);
            var secret2 = secretReference2.AsKeyVaultSecret(infra);
 
            // Should be idempotent
            _ = secretReference.AsKeyVaultSecret(infra);
 
            infra.Add(new ProvisioningOutput("secretUri1", typeof(string))
            {
                Value = secret.Properties.SecretUri
            });
 
            infra.Add(new ProvisioningOutput("secretUri2", typeof(string))
            {
                Value = secret2.Properties.SecretUri
            });
        });
 
        var (manifest, bicep) = await AzureManifestUtils.GetManifestWithBicep(module.Resource, skipPreparer: true);
 
        await Verify(manifest.ToString(), "json")
              .AppendContentAsFile(bicep, "bicep");
              
    }
 
    [Fact]
    public void GetSecret_ReturnsSecretReference()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var kv = builder.AddAzureKeyVault("myKeyVault");
        
        var secret = kv.GetSecret("mySecret");
        
        Assert.NotNull(secret);
        Assert.Equal("mySecret", secret.SecretName);
        Assert.Same(kv.Resource, secret.Resource);
    }
 
    [Fact]
    public void AddSecret_ReturnsSecretResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var secretParam = builder.AddParameter("secretParam", secret: true);
        var kv = builder.AddAzureKeyVault("myKeyVault");
        
        var secretResource = kv.AddSecret("mySecret", secretParam);
        
        Assert.NotNull(secretResource);
        Assert.IsType<AzureKeyVaultSecretResource>(secretResource.Resource);
        Assert.Equal("mySecret", secretResource.Resource.Name);
        Assert.Equal("mySecret", secretResource.Resource.SecretName);
        Assert.Same(kv.Resource, secretResource.Resource.Parent);
        Assert.Single(kv.Resource.Secrets);
        Assert.Same(secretResource.Resource, kv.Resource.Secrets[0]);
    }
 
    [Fact]
    public async Task AddSecret_WithParameterResource_GeneratesCorrectBicep()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var secret = builder.AddParameter("my-secret-param", secret: true);
        var kv = builder.AddAzureKeyVault("mykv");
        var secretResource = kv.AddSecret("my-secret", secret);
 
        var (manifest, bicep) = await AzureManifestUtils.GetManifestWithBicep(kv.Resource);
 
        await Verify(manifest.ToString(), "json")
              .AppendContentAsFile(bicep, "bicep");
    }
 
    [Fact]
    public async Task AddSecret_WithReferenceExpression_GeneratesCorrectBicep()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var pwd = builder.AddParameter("password", secret: true);
        var connectionString = ReferenceExpression.Create($"Server=localhost;Database=mydb;pwd={pwd}");
        var kv = builder.AddAzureKeyVault("mykv");
        var secretResource = kv.AddSecret("connection-string", connectionString);
 
        var (manifest, bicep) = await AzureManifestUtils.GetManifestWithBicep(kv.Resource);
 
        await Verify(manifest.ToString(), "json")
              .AppendContentAsFile(bicep, "bicep");
    }
 
    [Fact]
    public async Task AddSecret_WithMultipleSecrets_GeneratesCorrectBicep()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var secretParam = builder.AddParameter("secret-param", secret: true);
        var apiKey = builder.AddParameter("api-key", secret: true);
        var connectionString = ReferenceExpression.Create($"Server=localhost;Database=mydb;User=user");
 
        var kv = builder.AddAzureKeyVault("mykv");
        kv.AddSecret("my-secret", secretParam);
        kv.AddSecret("app-api-key", apiKey);
        kv.AddSecret("connection-string", connectionString);
 
        var (manifest, bicep) = await AzureManifestUtils.GetManifestWithBicep(kv.Resource);
 
        await Verify(manifest.ToString(), "json")
              .AppendContentAsFile(bicep, "bicep");
    }
 
    [Theory]
    [InlineData("")]
    [InlineData("   ")]
    public void AddSecret_WithEmptySecretName_ThrowsArgumentException(string invalidSecretName)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var secretParam = builder.AddParameter("secretParam", secret: true);
        var kv = builder.AddAzureKeyVault("myKeyVault");
 
        var exception = Assert.Throws<ArgumentException>(() => kv.AddSecret(invalidSecretName, secretParam));
        Assert.Contains("Secret name", exception.Message);
    }
 
    [Theory]
    [InlineData("secret_with_underscores")]
    [InlineData("secret.with.dots")]
    [InlineData("secret with spaces")]
    [InlineData("secret/with/slashes")]
    [InlineData("secret@with@symbols")]
    public void AddSecret_WithInvalidSecretName_ThrowsArgumentException(string invalidName)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var secretParam = builder.AddParameter("secretParam", secret: true);
        var kv = builder.AddAzureKeyVault("myKeyVault");
 
        var exception = Assert.Throws<ArgumentException>(() => kv.AddSecret(invalidName, secretParam));
        Assert.Contains("Secret name can only contain", exception.Message);
    }
 
    [Theory]
    [InlineData("valid-secret")]
    [InlineData("VALID-SECRET")]
    [InlineData("valid123")]
    [InlineData("a")]
    public void AddSecret_WithValidSecretName_DoesNotThrow(string validSecretName)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var secretParam = builder.AddParameter("secretParam", secret: true);
        var kv = builder.AddAzureKeyVault("myKeyVault");
 
        var exception = Record.Exception(() => kv.AddSecret(validSecretName, secretParam));
        Assert.Null(exception);
    }
 
    [Fact]
    public void AddSecret_WithTooLongSecretName_ThrowsArgumentException()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var secretParam = builder.AddParameter("secretParam", secret: true);
        var kv = builder.AddAzureKeyVault("myKeyVault");
 
        // Create a 128-character secret name (too long)
        var tooLongSecretName = new string('a', 128);
 
        var exception = Assert.Throws<ArgumentException>(() => kv.AddSecret(tooLongSecretName, secretParam));
        Assert.Contains("cannot be longer than 127 characters", exception.Message);
    }
}