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.Http.Json;
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;
using Aspire.Hosting.Tests.Dcp;
using System.Text.Json.Nodes;
 
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);
 
        var healthCheckTcs = new TaskCompletionSource<HealthCheckResult>();
        builder.Services.AddHealthChecks().AddAsyncCheck("blocking_check", () =>
        {
            return healthCheckTcs.Task;
        });
 
        var resource = builder.AddRedis("resource")
                           .WithHealthCheck("blocking_check");
 
        var dependentResource = builder.AddRedis("dependentresource")
                                       .WaitFor(resource);
 
        using var app = builder.Build();
 
        var pendingStart = app.StartAsync(cts.Token);
 
        var rns = app.Services.GetRequiredService<ResourceNotificationService>();
 
        await rns.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running, cts.Token);
 
        await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting, cts.Token);
 
        healthCheckTcs.SetResult(HealthCheckResult.Healthy());
 
        await rns.WaitForResourceHealthyAsync(resource.Resource.Name, cts.Token);
 
        await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, cts.Token);
 
        await pendingStart;
 
        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 VerifyDatabasesAreNotDuplicatedForPersistentRedisInsightContainer()
    {
        var randomResourceSuffix = Random.Shared.Next(10000).ToString();
        var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
 
        var configure = (DistributedApplicationOptions options) =>
        {
            options.ContainerRegistryOverride = ComponentTestConstants.AspireTestContainerRegistry;
        };
 
        using var builder1 = TestDistributedApplicationBuilder.Create(configure, testOutputHelper);
        builder1.Configuration[$"DcpPublisher:ResourceNameSuffix"] = randomResourceSuffix;
 
        IResourceBuilder<RedisInsightResource>? redisInsightBuilder = null;
        var redis1 = builder1.AddRedis("redisForInsightPersistence")
                .WithRedisInsight(c =>
                    {
                        redisInsightBuilder = c;
                        c.WithLifetime(ContainerLifetime.Persistent);
                    });
 
        // Wire up an additional event subcription to ResourceReadyEvent on the RedisInsightResource
        // instance. This works because the ResourceReadyEvent fires non-blocking sequential so the
        // wire-up that WithRedisInsight does is guaranteed to execute before this one does. So we then
        // use this to block pulling the list of databases until we know they've been updated. This
        // will repeated below for the second app.
        //
        // Issue: https://github.com/dotnet/aspire/issues/6455
        Assert.NotNull(redisInsightBuilder);
        var redisInsightsReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        builder1.Eventing.Subscribe<ResourceReadyEvent>(redisInsightBuilder.Resource, (evt, ct) =>
        {
            redisInsightsReady.TrySetResult();
            return Task.CompletedTask;
        });
 
        using var app1 = builder1.Build();
 
        await app1.StartAsync(cts.Token);
 
        await redisInsightsReady.Task.WaitAsync(cts.Token);
 
        using var client1 = app1.CreateHttpClient($"{redis1.Resource.Name}-insight", "http");
        var firstRunDatabases = await client1.GetFromJsonAsync<RedisInsightDatabaseModel[]>("/api/databases", cts.Token);
 
        Assert.NotNull(firstRunDatabases);
        Assert.Single(firstRunDatabases);
        Assert.Equal($"{redis1.Resource.Name}", firstRunDatabases[0].Name);
 
        await app1.StopAsync(cts.Token);
 
        using var builder2 = TestDistributedApplicationBuilder.Create(configure, testOutputHelper);
        builder2.Configuration[$"DcpPublisher:ResourceNameSuffix"] = randomResourceSuffix;
 
        var redis2 = builder2.AddRedis("redisForInsightPersistence")
                .WithRedisInsight(c =>
                {
                    redisInsightBuilder = c;
                    c.WithLifetime(ContainerLifetime.Persistent);
                });
 
        // Wire up an additional event subcription to ResourceReadyEvent on the RedisInsightResource
        // instance. This works because the ResourceReadyEvent fires non-blocking sequential so the
        // wire-up that WithRedisInsight does is guaranteed to execute before this one does. So we then
        // use this to block pulling the list of databases until we know they've been updated. This
        // will repeated below for the second app.
        Assert.NotNull(redisInsightBuilder);
        redisInsightsReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        builder2.Eventing.Subscribe<ResourceReadyEvent>(redisInsightBuilder.Resource, (evt, ct) =>
        {
            redisInsightsReady.TrySetResult();
            return Task.CompletedTask;
        });
 
        using var app2 = builder2.Build();
        await app2.StartAsync(cts.Token);
 
        await redisInsightsReady.Task.WaitAsync(cts.Token);
 
        using var client2 = app2.CreateHttpClient($"{redisInsightBuilder.Resource.Name}", "http");
        var secondRunDatabases = await client2.GetFromJsonAsync<RedisInsightDatabaseModel[]>("/api/databases", cts.Token);
 
        Assert.NotNull(secondRunDatabases);
        Assert.Single(secondRunDatabases);
        Assert.Equal($"{redis2.Resource.Name}", secondRunDatabases[0].Name);
        Assert.NotEqual(secondRunDatabases.Single().Id, firstRunDatabases.Single().Id);
 
        // HACK: This is a workaround for the fact that ApplicationExecutor is not a public type. What I have
        //       done here is I get the latest event from RNS for the insights instance which gives me the resource
        //       name as known from a DCP perspective. I then use the ApplicationExecutorProxy (introduced with this
        //       change to call the ApplicationExecutor stop method. The proxy is a public type with an internal
        //       constructor inside the Aspire.Hosting.Tests package. This is a short term solution for 9.0 to
        //       make sure that we have good test coverage for WithRedisInsight behavior, but we need a better
        //       long term solution in 9.x for folks that will want to do things like execute commands against
        //       resources to stop specific containers.
        var rns = app2.Services.GetRequiredService<ResourceNotificationService>();
        var latestEvent = await rns.WaitForResourceHealthyAsync(redisInsightBuilder.Resource.Name, cts.Token);
        var executorProxy = app2.Services.GetRequiredService<ApplicationExecutorProxy>();
        await executorProxy.StopResourceAsync(latestEvent.ResourceId, cts.Token);
 
        await app2.StopAsync(cts.Token);
    }
 
    [Fact]
    [RequiresDocker]
    public async Task VerifyWithRedisInsightImportDatabases()
    {
        var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
 
        using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
 
        var redis1 = builder.AddRedis("redis-1");
        IResourceBuilder<RedisInsightResource>? redisInsightBuilder = null;
        var redis2 = builder.AddRedis("redis-2").WithRedisInsight(c => redisInsightBuilder = c);
        Assert.NotNull(redisInsightBuilder);
 
        // RedisInsight will import databases when it is ready, this task will run after the initial databases import
        // so we will use that to know when the databases have been successfully imported
        var redisInsightsReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        builder.Eventing.Subscribe<ResourceReadyEvent>(redisInsightBuilder.Resource, (evt, ct) =>
        {
            redisInsightsReady.TrySetResult();
            return Task.CompletedTask;
        });
 
        using var app = builder.Build();
 
        await app.StartAsync();
 
        await redisInsightsReady.Task.WaitAsync(cts.Token);
 
        var client = app.CreateHttpClient(redisInsightBuilder.Resource.Name, "http");
 
        var response = await client.GetAsync("/api/databases", cts.Token);
        response.EnsureSuccessStatusCode();
 
        var databases = await response.Content.ReadFromJsonAsync<List<RedisInsightDatabaseModel>>(cts.Token);
 
        Assert.NotNull(databases);
        Assert.Collection(databases,
        db =>
        {
            Assert.Equal(redis1.Resource.Name, db.Name);
            Assert.Equal(redis1.Resource.Name, db.Host);
            Assert.Equal(redis1.Resource.PrimaryEndpoint.TargetPort, db.Port);
            Assert.Equal("STANDALONE", db.ConnectionType);
            Assert.Equal(0, db.Db);
        },
        db =>
        {
            Assert.Equal(redis2.Resource.Name, db.Name);
            Assert.Equal(redis2.Resource.Name, db.Host);
            Assert.Equal(redis2.Resource.PrimaryEndpoint.TargetPort, db.Port);
            Assert.Equal("STANDALONE", db.ConnectionType);
            Assert.Equal(0, db.Db);
        });
 
        foreach (var db in databases)
        {
            var cts2 = new CancellationTokenSource(TimeSpan.FromSeconds(2));
            var testConnectionResponse = await client.GetAsync($"/api/databases/test/{db.Id}", cts2.Token);
            response.EnsureSuccessStatusCode();
        }
    }
 
    [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.Generate(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();
        }
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    [RequiresDocker]
    public async Task RedisInsightWithDataShouldPersistStateBetweenUsages(bool useVolume)
    {
        var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10));
 
        string? volumeName = null;
        string? bindMountPath = null;
 
        try
        {
            using var builder1 = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
            IResourceBuilder<RedisInsightResource>? redisInsightBuilder1 = null;
            var redis1 = builder1.AddRedis("redis")
                .WithRedisInsight(c => { redisInsightBuilder1 = c; });
            Assert.NotNull(redisInsightBuilder1);
 
            if (useVolume)
            {
                // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails
                volumeName = VolumeNameGenerator.Generate(redisInsightBuilder1, nameof(RedisInsightWithDataShouldPersistStateBetweenUsages));
 
                // if the volume already exists (because of a crashing previous run), delete it
                DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true);
                redisInsightBuilder1.WithDataVolume(volumeName);
            }
            else
            {
                bindMountPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
                redisInsightBuilder1.WithDataBindMount(bindMountPath);
            }
 
            using (var app = builder1.Build())
            {
                await app.StartAsync();
 
                // RedisInsight will import databases when it is ready, this task will run after the initial databases import
                // so we will use that to know when the databases have been successfully imported
                var redisInsightsReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
                builder1.Eventing.Subscribe<ResourceReadyEvent>(redisInsightBuilder1.Resource, (evt, ct) =>
                {
                    redisInsightsReady.TrySetResult();
                    return Task.CompletedTask;
                });
 
                await redisInsightsReady.Task.WaitAsync(cts.Token);
 
                try
                {
                    var httpClient = app.CreateHttpClient(redisInsightBuilder1.Resource.Name, "http");
                    await AcceptRedisInsightEula(httpClient, cts.Token);
                }
                finally
                {
                    // Stops the container, or the Volume would still be in use
                    await app.StopAsync();
                }
            }
 
            using var builder2 = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
 
            IResourceBuilder<RedisInsightResource>? redisInsightBuilder2 = null;
            var redis2 = builder2.AddRedis("redis")
                .WithRedisInsight(c => { redisInsightBuilder2 = c; });
            Assert.NotNull(redisInsightBuilder2);
 
            if (useVolume)
            {
                redisInsightBuilder2.WithDataVolume(volumeName);
            }
            else
            {
                redisInsightBuilder2.WithDataBindMount(bindMountPath!);
            }
 
            using (var app = builder2.Build())
            {
                await app.StartAsync();
 
                // RedisInsight will import databases when it is ready, this task will run after the initial databases import
                // so we will use that to know when the databases have been successfully imported
                var redisInsightsReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
                builder2.Eventing.Subscribe<ResourceReadyEvent>(redisInsightBuilder2.Resource, (evt, ct) =>
                {
                    redisInsightsReady.TrySetResult();
                    return Task.CompletedTask;
                });
 
                await redisInsightsReady.Task.WaitAsync(cts.Token);
 
                try
                {
                    var httpClient = app.CreateHttpClient(redisInsightBuilder2.Resource.Name, "http");
                    await EnsureRedisInsightEulaAccepted(httpClient, cts.Token);
                }
                finally
                {
                    // Stops the container, or the Volume would still be in use
                    await app.StopAsync();
                }
            }
        }
        finally
        {
            if (volumeName is not null)
            {
                DockerUtils.AttemptDeleteDockerVolume(volumeName);
            }
 
            if (bindMountPath is not null)
            {
                try
                {
                    Directory.Delete(bindMountPath, recursive: true);
                }
                catch
                {
                    // Don't fail test if we can't clean the temporary folder
                }
            }
        }
    }
 
    private static async Task EnsureRedisInsightEulaAccepted(HttpClient httpClient, CancellationToken ct)
    {
        var response = await httpClient.GetAsync("/api/settings", ct);
        response.EnsureSuccessStatusCode();
 
        var content = await response.Content.ReadAsStringAsync(ct);
 
        var jo = JsonObject.Parse(content);
        Assert.NotNull(jo);
        var agreements = jo["agreements"];
 
        Assert.NotNull(agreements);
        Assert.False(agreements["analytics"]!.GetValue<bool>());
        Assert.False(agreements["notifications"]!.GetValue<bool>());
        Assert.False(agreements["encryption"]!.GetValue<bool>());
        Assert.True(agreements["eula"]!.GetValue<bool>());
    }
 
    static async Task AcceptRedisInsightEula(HttpClient client, CancellationToken ct)
    {
        var jsonContent = JsonContent.Create(new
        {
            agreements = new
            {
                eula = true,
                analytics = false,
                notifications = false,
                encryption = false,
            }
        });
 
        var apiUrl = $"/api/settings";
 
        var response = await client.PatchAsync(apiUrl, jsonContent, ct);
 
        response.EnsureSuccessStatusCode();
 
        await EnsureRedisInsightEulaAccepted(client, ct);
    }
 
    internal sealed class RedisInsightDatabaseModel
    {
        public string? Id { get; set; }
        public string? Host { get; set; }
        public int? Port { get; set; }
        public string? Name { get; set; }
        public int? Db { get; set; }
        public string? ConnectionType { get; set; }
    }
}