File: AzureRedisEnterpriseExtensionsTests.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 ASPIREAZUREREDIS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 
using System.Net.Sockets;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
using static Aspire.Hosting.Utils.AzureManifestUtils;
 
namespace Aspire.Hosting.Azure.Tests;
 
public class AzureRedisEnterpriseExtensionsTests
{
    /// <summary>
    /// Test both with and without ACA infrastructure because the role assignments
    /// are handled differently between the two. This ensures that the bicep is generated
    /// consistently regardless of the infrastructure used in RunMode.
    /// </summary>
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task AddAzureRedisEnterprise(bool useAcaInfrastructure)
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run);
 
        if (useAcaInfrastructure)
        {
            builder.AddAzureContainerAppEnvironment("env");
        }
 
        var redis = builder.AddAzureRedisEnterprise("redis-cache");
 
        builder.AddContainer("api", "myimage")
            .WithReference(redis);
 
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var (_, bicep) = await GetManifestWithBicep(model, redis.Resource);
        var redisRoles = Assert.Single(model.Resources.OfType<AzureProvisioningResource>(), r => r.Name == "redis-cache-roles");
        var (_, redisRolesBicep) = await GetManifestWithBicep(redisRoles, skipPreparer: true);
 
        await Verify(bicep, "bicep")
              .AppendContentAsFile(redisRolesBicep, "bicep");
    }
 
    [Fact]
    public async Task AddAzureRedisEnterpriseRunAsContainerProducesCorrectConnectionString()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        RedisResource? redisResource = null;
        var redis = builder.AddAzureRedisEnterprise("cache")
            .RunAsContainer(c =>
            {
                redisResource = c.Resource;
 
                c.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 12455));
            });
 
        Assert.True(redis.Resource.IsContainer(), "The resource should now be a container resource.");
 
        Assert.NotNull(redisResource?.PasswordParameter);
        Assert.Equal($"localhost:12455,password={await redisResource.PasswordParameter.GetValueAsync(CancellationToken.None)}", await redis.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None));
    }
 
    [Fact]
    public async Task AddAzureRedisEnterpriseRunAsContainerProducesCorrectHostAndPassword()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var pass = builder.AddParameter("pass", "p@ssw0rd1");
 
        RedisResource? redisResource = null;
        var redis = builder.AddAzureRedisEnterprise("cache")
            .RunAsContainer(c =>
            {
                redisResource = c.Resource;
 
                c.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 12455))
                    .WithHostPort(12455)
                    .WithPassword(pass);
            });
 
        Assert.NotNull(redisResource);
 
        var endpoint = Assert.Single(redisResource.Annotations.OfType<EndpointAnnotation>());
        Assert.Equal(6379, endpoint.TargetPort);
        Assert.False(endpoint.IsExternal);
        Assert.Equal("tcp", endpoint.Name);
        Assert.Equal(12455, endpoint.Port);
        Assert.Equal(ProtocolType.Tcp, endpoint.Protocol);
        Assert.Equal("tcp", endpoint.Transport);
        Assert.Equal("tcp", endpoint.UriScheme);
 
        Assert.True(redis.Resource.IsContainer(), "The resource should now be a container resource.");
 
        Assert.NotNull(redisResource?.PasswordParameter);
        Assert.Equal($"localhost:12455,password=p@ssw0rd1", await redis.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None));
 
        // Test the new reference properties
        Assert.NotNull(redis.Resource.HostName);
        Assert.Equal("localhost:12455", await redis.Resource.HostName.GetValueAsync(CancellationToken.None));
 
        Assert.NotNull(redis.Resource.Password);
        Assert.Equal("p@ssw0rd1", await redis.Resource.Password.GetValueAsync(CancellationToken.None));
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public void RunAsContainerAppliesAnnotationsCorrectly(bool before)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var cache = builder.AddAzureRedisEnterprise("cache");
 
        if (before)
        {
            cache.WithAnnotation(new Dummy1Annotation());
        }
 
        cache.RunAsContainer(c =>
        {
            c.WithAnnotation(new Dummy2Annotation());
        });
 
        if (!before)
        {
            cache.WithAnnotation(new Dummy1Annotation());
        }
 
        var cacheInModel = builder.Resources.Single(r => r.Name == "cache");
 
        Assert.True(cacheInModel.TryGetAnnotationsOfType<Dummy1Annotation>(out var cacheAnnotations1));
        Assert.Single(cacheAnnotations1);
 
        Assert.True(cacheInModel.TryGetAnnotationsOfType<Dummy2Annotation>(out var cacheAnnotations2));
        Assert.Single(cacheAnnotations2);
    }
 
    [Fact]
    public async Task AddAzureRedisEnterpriseWithAutoGeneratedPasswordProducesCorrectValues()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var redis = builder.AddAzureRedisEnterprise("redis-test")
                           .RunAsContainer(c =>
                           {
                               c.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6379));
                           });
 
        // Even with auto-generated password, Password and HostName should be available
        Assert.NotNull(redis.Resource.HostName);
        Assert.NotNull(redis.Resource.Password);
 
        // Validate the values can be resolved
        var hostValue = await redis.Resource.HostName.GetValueAsync(CancellationToken.None);
        Assert.Equal("localhost:6379", hostValue);
 
        var passwordValue = await redis.Resource.Password.GetValueAsync(CancellationToken.None);
        Assert.NotNull(passwordValue);
        Assert.NotEmpty(passwordValue);
    }
 
    [Fact]
    public void AddAzureRedisEnterpriseEntraIdModePasswordIsNull()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        var redis = builder.AddAzureRedisEnterprise("redis-data");
 
        // In Azure mode, Password should be null since Redis uses connection strings
        Assert.Null(redis.Resource.Password);
 
        // HostName should still be available and resolve to bicep output
        Assert.NotNull(redis.Resource.HostName);
        Assert.Equal("{redis-data.outputs.hostName}", redis.Resource.HostName.ValueExpression);
    }
 
    private sealed class Dummy1Annotation : IResourceAnnotation
    {
    }
 
    private sealed class Dummy2Annotation : IResourceAnnotation
    {
    }
 
    [Fact]
    public void AddAsExistingResource_ShouldBeIdempotent_ForAzureRedisCacheResource()
    {
        // Arrange
        var redisResource = new AzureRedisEnterpriseResource("test-redis", _ => { });
        var infrastructure = new AzureResourceInfrastructure(redisResource, "test-redis");
 
        // Act - Call AddAsExistingResource twice
        var firstResult = redisResource.AddAsExistingResource(infrastructure);
        var secondResult = redisResource.AddAsExistingResource(infrastructure);
 
        // Assert - Both calls should return the same resource instance, not duplicates
        Assert.Same(firstResult, secondResult);
    }
 
    [Fact]
    public async Task AddAsExistingResource_RespectsExistingAzureResourceAnnotation_ForAzureRedisCacheResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var existingName = builder.AddParameter("existing-redis-name");
        var existingResourceGroup = builder.AddParameter("existing-redis-rg");
 
        var redis = builder.AddAzureRedisEnterprise("test-redis")
            .AsExisting(existingName, existingResourceGroup);
 
        var module = builder.AddAzureInfrastructure("mymodule", infra =>
        {
            _ = redis.Resource.AddAsExistingResource(infra);
        });
 
        var (manifest, bicep) = await GetManifestWithBicep(module.Resource, skipPreparer: true);
 
        await Verify(bicep, "bicep");
    }
 
    [Theory]
    [InlineData(null)]
    [InlineData("mykeyvault")]
    public async Task AddAzureRedisEnterpriseWithAccessKeyAuthentication(string? kvName)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
 
        var redis = builder.AddAzureRedisEnterprise("redis-cache");
 
        if (kvName is null)
        {
            redis.WithAccessKeyAuthentication();
        }
        else
        {
            redis.WithAccessKeyAuthentication(builder.AddAzureKeyVault(kvName));
        }
 
        var (_, bicep) = await GetManifestWithBicep(redis.Resource);
 
        await Verify(bicep, extension: "bicep");
    }
}