File: RedisFunctionalTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Redis.Tests\Aspire.Hosting.Redis.Tests.csproj (Aspire.Hosting.Redis.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Net;
using Aspire.Components.Common.Tests;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Testing;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using StackExchange.Redis;
using Xunit;
using Xunit.Abstractions;
 
namespace Aspire.Hosting.Redis.Tests;
 
public class RedisFunctionalTests(ITestOutputHelper testOutputHelper)
{
    [Fact]
    [RequiresDocker]
    public async Task VerifyWaitForOnRedisBlocksDependentResources()
    {
        var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
        using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
 
        // We use the following check added to the Redis resource to block
        // dependent reosurces from starting. This means we'll have two checks
        // associated with the redis resource ... the built in one and the
        // one that we add here. We'll manipulate the TCS to allow us to check
        // states at various stages of the execution.
        var healthCheckTcs = new TaskCompletionSource<HealthCheckResult>();
        builder.Services.AddHealthChecks().AddAsyncCheck("blocking_check", () =>
        {
            return healthCheckTcs.Task;
        });
 
        var redis = builder.AddRedis("redis")
                           .WithHealthCheck("blocking_check");
 
        var dependentResource = builder.AddRedis("dependentresource")
                                       .WaitFor(redis); // Just using another redis instance as a dependent resource.
 
        using var app = builder.Build();
 
        var pendingStart = app.StartAsync(cts.Token);
 
        var rns = app.Services.GetRequiredService<ResourceNotificationService>();
 
        // What for the Redis server to start.
        await rns.WaitForResourceAsync(redis.Resource.Name, KnownResourceStates.Running, cts.Token);
 
        // Wait for the dependent resource to be in the Waiting state.
        await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting, cts.Token);
 
        // Now unblock the health check.
        healthCheckTcs.SetResult(HealthCheckResult.Healthy());
 
        // ... and wait for the resource as a whole to move into the health state.
        await rns.WaitForResourceAsync(redis.Resource.Name, (re => re.Snapshot.HealthStatus == HealthStatus.Healthy), cts.Token);
 
        // ... then the dependent resource should be able to move into a running state.
        await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, cts.Token);
 
        await pendingStart; // Startup should now complete.
 
        // ... but we'll shut everything down immediately because we are done.
        await app.StopAsync();
    }
 
    [Fact]
    [RequiresDocker]
    public async Task VerifyRedisCommanderResource()
    {
        using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
 
        IResourceBuilder<RedisCommanderResource>? commanderBuilder = null;
        var redis = builder.AddRedis("redis").WithRedisCommander(c => commanderBuilder = c);
        Assert.NotNull(commanderBuilder);
 
        using var app = builder.Build();
 
        await app.StartAsync();
 
        await app.WaitForTextAsync("Redis Connection", resourceName: commanderBuilder.Resource.Name);
 
        var client = app.CreateHttpClient(commanderBuilder.Resource.Name, "http");
 
        var endpoint = redis.GetEndpoint("tcp");
        var path = $"/apiv2/server/R:{redis.Resource.Name}:{endpoint.TargetPort}:0/info";
        var response = await client.GetAsync(path);
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }
 
    [Fact]
    [RequiresDocker]
    public async Task VerifyRedisResource()
    {
        using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
 
        var redis = builder.AddRedis("redis");
 
        using var app = builder.Build();
 
        await app.StartAsync();
 
        var hb = Host.CreateApplicationBuilder();
 
        hb.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
        {
            [$"ConnectionStrings:{redis.Resource.Name}"] = await redis.Resource.GetConnectionStringAsync()
        });
 
        hb.AddRedisClient(redis.Resource.Name);
 
        using var host = hb.Build();
 
        await host.StartAsync();
 
        var redisClient = host.Services.GetRequiredService<IConnectionMultiplexer>();
 
        var db = redisClient.GetDatabase();
 
        await db.StringSetAsync("key", "value");
 
        var value = await db.StringGetAsync("key");
 
        Assert.Equal("value", value);
    }
 
    [Fact]
    [RequiresDocker]
    public async Task WithDataVolumeShouldPersistStateBetweenUsages()
    {
        // Use a volume to do a snapshot save
 
        using var builder1 = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
        var redis1 = builder1.AddRedis("redis");
 
        // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails
        var volumeName = VolumeNameGenerator.CreateVolumeName(redis1, nameof(WithDataVolumeShouldPersistStateBetweenUsages));
        redis1.WithDataVolume(volumeName);
        // if the volume already exists (because of a crashing previous run), delete it
        DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true);
 
        using (var app = builder1.Build())
        {
            await app.StartAsync();
 
            var hb = Host.CreateApplicationBuilder();
 
            // BGSAVE is only available in admin mode, enable it for this instance
            hb.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
            {
                [$"ConnectionStrings:{redis1.Resource.Name}"] = $"{await redis1.Resource.GetConnectionStringAsync()},allowAdmin=true"
            });
 
            hb.AddRedisClient(redis1.Resource.Name);
 
            using (var host = hb.Build())
            {
                await host.StartAsync();
 
                var redisClient = host.Services.GetRequiredService<IConnectionMultiplexer>();
 
                var db = redisClient.GetDatabase();
 
                await db.StringSetAsync("key", "value");
 
                // Force Redis to save the keys (snapshotting)
                // c.f. https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/
 
                await redisClient.GetServers().First().SaveAsync(SaveType.BackgroundSave);
            }
 
            // Stops the container, or the Volume would still be in use
            await app.StopAsync();
        }
 
        using var builder2 = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
        var redis2 = builder2.AddRedis("redis").WithDataVolume(volumeName);
 
        using (var app = builder2.Build())
        {
            await app.StartAsync();
 
            var hb = Host.CreateApplicationBuilder();
 
            hb.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
            {
                [$"ConnectionStrings:{redis2.Resource.Name}"] = await redis2.Resource.GetConnectionStringAsync()
            });
 
            hb.AddRedisClient(redis2.Resource.Name);
 
            using (var host = hb.Build())
            {
                await host.StartAsync();
 
                var redisClient = host.Services.GetRequiredService<IConnectionMultiplexer>();
 
                var db = redisClient.GetDatabase();
 
                var value = await db.StringGetAsync("key");
 
                Assert.Equal("value", value);
            }
 
            // Stops the container, or the Volume would still be in use
            await app.StopAsync();
        }
 
        DockerUtils.AttemptDeleteDockerVolume(volumeName);
    }
 
    [Fact]
    [RequiresDocker]
    public async Task WithDataBindMountShouldPersistStateBetweenUsages()
    {
        var bindMountPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
 
        if (!Directory.Exists(bindMountPath))
        {
            Directory.CreateDirectory(bindMountPath);
        }
 
        // Use a bind mount to do a snapshot save
 
        using var builder1 = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
        var redis1 = builder1.AddRedis("redis").WithDataBindMount(bindMountPath);
 
        using (var app = builder1.Build())
        {
            await app.StartAsync();
 
            var hb = Host.CreateApplicationBuilder();
 
            // BGSAVE is only available in admin mode, enable it for this instance
            hb.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
            {
                [$"ConnectionStrings:{redis1.Resource.Name}"] = $"{await redis1.Resource.GetConnectionStringAsync()},allowAdmin=true"
            });
 
            hb.AddRedisClient(redis1.Resource.Name);
 
            using (var host = hb.Build())
            {
                await host.StartAsync();
 
                var redisClient = host.Services.GetRequiredService<IConnectionMultiplexer>();
 
                var db = redisClient.GetDatabase();
 
                await db.StringSetAsync("key", "value");
 
                // Force Redis to save the keys (snapshotting)
                // c.f. https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/
 
                await redisClient.GetServers().First().SaveAsync(SaveType.BackgroundSave);
            }
 
            await app.StopAsync();
        }
 
        using var builder2 = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
        var redis2 = builder2.AddRedis("redis").WithDataBindMount(bindMountPath);
 
        using (var app = builder2.Build())
        {
            await app.StartAsync();
 
            var hb = Host.CreateApplicationBuilder();
 
            hb.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
            {
                [$"ConnectionStrings:{redis2.Resource.Name}"] = await redis2.Resource.GetConnectionStringAsync()
            });
 
            hb.AddRedisClient(redis2.Resource.Name);
 
            using (var host = hb.Build())
            {
                await host.StartAsync();
 
                var redisClient = host.Services.GetRequiredService<IConnectionMultiplexer>();
 
                var db = redisClient.GetDatabase();
 
                var value = await db.StringGetAsync("key");
 
                Assert.Equal("value", value);
            }
 
            await app.StopAsync();
        }
 
        try
        {
            Directory.Delete(bindMountPath, recursive: true);
        }
        catch
        {
            // Don't fail test if we can't clean the temporary folder
        }
    }
 
    [Fact]
    [RequiresDocker]
    public async Task PersistenceIsDisabledByDefault()
    {
        // Checks that without enabling Redis Persistence the tests fail
 
        using var builder1 = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
        var redis1 = builder1.AddRedis("redis");
 
        using (var app = builder1.Build())
        {
            await app.StartAsync();
 
            var hb = Host.CreateApplicationBuilder();
 
            // BGSAVE is only available in admin mode, enable it for this instance
            hb.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
            {
                [$"ConnectionStrings:{redis1.Resource.Name}"] = $"{await redis1.Resource.GetConnectionStringAsync()},allowAdmin=true"
            });
 
            hb.AddRedisClient(redis1.Resource.Name);
 
            using (var host = hb.Build())
            {
                await host.StartAsync();
 
                var redisClient = host.Services.GetRequiredService<IConnectionMultiplexer>();
 
                var db = redisClient.GetDatabase();
 
                await db.StringSetAsync("key", "value");
            }
 
            await app.StopAsync();
        }
 
        using var builder2 = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
        var redis2 = builder2.AddRedis("redis");
 
        using (var app = builder2.Build())
        {
            await app.StartAsync();
 
            var hb = Host.CreateApplicationBuilder();
 
            hb.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
            {
                [$"ConnectionStrings:{redis2.Resource.Name}"] = await redis2.Resource.GetConnectionStringAsync()
            });
 
            hb.AddRedisClient(redis2.Resource.Name);
 
            using (var host = hb.Build())
            {
                await host.StartAsync();
 
                var redisClient = host.Services.GetRequiredService<IConnectionMultiplexer>();
 
                var db = redisClient.GetDatabase();
 
                var value = await db.StringGetAsync("key");
 
                Assert.True(value.IsNull);
            }
 
            await app.StopAsync();
        }
    }
}