File: ReplicaSet\AddMongoDBReplicaSetTests.cs
Web Access
Project: src\tests\Aspire.Hosting.MongoDB.Tests\Aspire.Hosting.MongoDB.Tests.csproj (Aspire.Hosting.MongoDB.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.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Eventing;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Bson;
using MongoDB.Driver;
 
#pragma warning disable ASPIRECERTIFICATES001
#pragma warning disable ASPIREMONGODB001
 
namespace Aspire.Hosting.MongoDB.Tests;
 
public class AddMongoDBReplicaSetTests(ITestOutputHelper testOutputHelper)
{
    [Fact]
    public void AddMongoDBReplicaSetAddsHealthCheckAnnotationToResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var rs = builder.AddMongoDBReplicaSet("rs0");
        Assert.Single(rs.Resource.Annotations, a => a is HealthCheckAnnotation hca && hca.Key == "rs0_check");
    }
 
    [Fact]
    public void AddMongoDBReplicaSetAddsResourceToModel()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        builder.AddMongoDBReplicaSet("rs0");
 
        using var app = builder.Build();
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        var resource = Assert.Single(appModel.Resources.OfType<MongoDBReplicaSetResource>());
        Assert.Equal("rs0", resource.Name);
    }
 
    [Fact]
    public void WithMemberWaitsForTheMemberToStartRatherThanToBecomeHealthy()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var mongo1 = builder.AddMongoDB("mongo1");
        var rs = builder.AddMongoDBReplicaSet("rs0")
            .WithMember(mongo1);
 
        var wait = Assert.Single(rs.Resource.Annotations.OfType<WaitAnnotation>(),
            a => a.Resource == mongo1.Resource);
 
        // NOTE: Waiting for health here would deadlock a fresh replica set. A member started with `--replSet` has no primary
        // until this resource initiates the set against it, so its health can only follow the initialization, never precede
        // it.
        Assert.Equal(WaitType.WaitUntilStarted, wait.WaitType);
    }
 
    [Fact]
    public async Task WithMemberConfiguresServerWithReplSetArg()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var mongo1 = builder.AddMongoDB("mongo1");
        builder.AddMongoDBReplicaSet("rs0")
            .WithMember(mongo1);
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(mongo1.Resource);
        Assert.Contains("--replSet", args);
        Assert.Contains("rs0", args);
    }
 
    [Fact]
    public async Task ReplicaSetConnectionStringHasCorrectContents()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var mongo1 = builder.AddMongoDB("mongo1")
            .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 27017));
        var mongo2 = builder.AddMongoDB("mongo2")
            .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 27018));
 
        var rs = builder.AddMongoDBReplicaSet("rs0")
            .WithMember(mongo1)
            .WithMember(mongo2);
 
        using var app = builder.Build();
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        var replicaSetResource = Assert.Single(appModel.Resources.OfType<MongoDBReplicaSetResource>());
 
        var connectionString = await replicaSetResource.ConnectionStringExpression.GetValueAsync(CancellationToken.None);
 
        var connectionStringObj = new MongoUrl(connectionString);
        Assert.Equal("rs0", connectionStringObj.ReplicaSetName);
        Assert.Equal(["localhost:27017", "localhost:27018"], connectionStringObj.Servers.Select(s => s.ToString()));
    }
 
    [Fact]
    public async Task ReplicaSetConnectionStringWithSingleMemberHasNoTrailingComma()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var mongo1 = builder.AddMongoDB("mongo1")
            .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 27017));
 
        var rs = builder.AddMongoDBReplicaSet("rs0")
            .WithMember(mongo1);
 
        using var app = builder.Build();
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        var replicaSetResource = Assert.Single(appModel.Resources.OfType<MongoDBReplicaSetResource>());
 
        var connectionString = await replicaSetResource.ConnectionStringExpression.GetValueAsync(CancellationToken.None);
 
        var connectionStringObj = new MongoUrl(connectionString);
        Assert.Equal("rs0", connectionStringObj.ReplicaSetName);
        Assert.Equal(["localhost:27017"], connectionStringObj.Servers.Select(s => s.ToString()));
    }
 
    [Fact]
    public void ConnectionStringExpressionThrowsWhenNoMembersAdded()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var rs = builder.AddMongoDBReplicaSet("rs0");
 
        using var app = builder.Build();
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        var replicaSetResource = Assert.Single(appModel.Resources.OfType<MongoDBReplicaSetResource>());
 
        Assert.Throws<InvalidOperationException>(() => replicaSetResource.ConnectionStringExpression);
    }
 
    [Fact]
    public void AddMongoDBReplicaSetThrowsWhenBuilderIsNull()
    {
        IDistributedApplicationBuilder builder = null!;
        const string name = "rs0";
 
        var action = () => builder.AddMongoDBReplicaSet(name);
 
        var exception = Assert.Throws<ArgumentNullException>(action);
        Assert.Equal(nameof(builder), exception.ParamName);
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public void AddMongoDBReplicaSetThrowsWhenNameIsNullOrEmpty(bool isNull)
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var name = isNull ? null! : string.Empty;
 
        var action = () => builder.AddMongoDBReplicaSet(name);
 
        var exception = isNull
            ? Assert.Throws<ArgumentNullException>(action)
            : Assert.Throws<ArgumentException>(action);
        Assert.Equal(nameof(name), exception.ParamName);
    }
 
    [Fact]
    public void AddMongoDBReplicaSetThrowsInPublishMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
 
        Assert.Throws<NotSupportedException>(() => builder.AddMongoDBReplicaSet("rs0"));
    }
 
    [Fact]
    public void WithMemberOptsTheMemberInToTheDeveloperCertificate()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var mongo1 = builder.AddMongoDB("mongo1");
        builder.AddMongoDBReplicaSet("rs0").WithMember(mongo1);
 
        var annotation = Assert.Single(mongo1.Resource.Annotations.OfType<HttpsCertificateAnnotation>());
        Assert.True(annotation.UseDeveloperCertificate);
    }
 
    [Fact]
    public void WithMemberKeepsTheMembersOwnCertificateConfiguration()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var certificatePassword = builder.AddParameter("cert-password", "test123");
        var mongo1 = builder.AddMongoDB("mongo1").WithHttpsDeveloperCertificate(certificatePassword);
        builder.AddMongoDBReplicaSet("rs0").WithMember(mongo1);
 
        var annotation = Assert.Single(mongo1.Resource.Annotations.OfType<HttpsCertificateAnnotation>());
        Assert.Equal(certificatePassword.Resource, annotation.Password);
    }
 
    [Fact]
    public async Task WithMemberAllowsInvalidCertificatesForIntraClusterConnections()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        using var certificate = CreateTestCertificate();
        var mongo1 = builder.AddMongoDB("mongo1").WithHttpsCertificate(certificate);
        builder.AddMongoDBReplicaSet("rs0").WithMember(mongo1);
 
        using var app = builder.Build();
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        await builder.Eventing.PublishAsync(new BeforeStartEvent(app.Services, appModel));
 
        var args = await ArgumentEvaluator.GetArgumentListAsync(mongo1.Resource);
        Assert.Contains("--tlsAllowInvalidCertificates", args);
    }
 
    [Fact]
    public void WithMemberAcceptsFiftyMembersAndRejectsTheFiftyFirst()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var rs = builder.AddMongoDBReplicaSet("rs0");
 
        // NOTE: MongoDB allows a replica set to hold at most 50 members.
        for (var i = 1; i <= 50; i++)
        {
            rs.WithMember(builder.AddMongoDB($"mongo{i}"));
        }
 
        Assert.Equal(50, rs.Resource.Members.Count());
 
        var tooMany = builder.AddMongoDB("mongo51");
        var exception = Assert.Throws<InvalidOperationException>(() => rs.WithMember(tooMany));
        Assert.Contains("maximum of 50 members", exception.Message);
 
        // NOTE: Rejected before anything was mutated, so the member is still usable elsewhere.
        Assert.Null(tooMany.Resource.ReplicaSetName);
        Assert.False(tooMany.Resource.HasAnnotationOfType<MongoDBServerKeyFileAnnotation>());
        Assert.Equal(50, rs.Resource.Members.Count());
    }
 
    [Fact]
    public void WithMemberThrowsWhenTheSameMemberIsAddedTwice()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var mongo1 = builder.AddMongoDB("mongo1");
        var rs = builder.AddMongoDBReplicaSet("rs0").WithMember(mongo1);
 
        var action = () => rs.WithMember(mongo1);
 
        var exception = Assert.Throws<InvalidOperationException>(action);
        Assert.Contains("already been added as a member", exception.Message);
    }
 
    [Fact]
    public void WithMemberThrowsWhenTheMemberBelongsToAnotherReplicaSet()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var mongo1 = builder.AddMongoDB("mongo1");
        builder.AddMongoDBReplicaSet("rs0").WithMember(mongo1);
        var otherReplicaSet = builder.AddMongoDBReplicaSet("rs1");
 
        var action = () => otherReplicaSet.WithMember(mongo1);
 
        var exception = Assert.Throws<InvalidOperationException>(action);
        Assert.Contains("already a member of the replica set 'rs0'", exception.Message);
    }
 
    [Fact]
    public void WithMemberThrowsWhenTheMemberHasAConflictingPassword()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var memberPassword = builder.AddParameter("member-password", "p@ssw0rd", secret: true);
        var mongo1 = builder.AddMongoDB("mongo1", password: memberPassword);
        var rs = builder.AddMongoDBReplicaSet("rs0");
 
        var action = () => rs.WithMember(mongo1);
 
        var exception = Assert.Throws<InvalidOperationException>(action);
        Assert.Contains("explicit password", exception.Message);
        Assert.Contains(nameof(MongoDBReplicaSetBuilderExtensions.AddMongoDBReplicaSet), exception.Message);
    }
 
    [Fact]
    public void WithMemberThrowsWhenTheMemberHasAConflictingUserName()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var memberUserName = builder.AddParameter("member-username", "someone");
        var mongo1 = builder.AddMongoDB("mongo1", userName: memberUserName);
        var rs = builder.AddMongoDBReplicaSet("rs0");
 
        var action = () => rs.WithMember(mongo1);
 
        var exception = Assert.Throws<InvalidOperationException>(action);
        Assert.Contains("explicit user name", exception.Message);
    }
 
    [Fact]
    public void WithMemberThrowsWhenTheMemberHasAKeyFileOfItsOwn()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var mongo1 = builder.AddMongoDB("mongo1").WithKeyFile(new ParameterResource("own-key", _ => "own-key"));
 
        var action = () => builder.AddMongoDBReplicaSet("rs0").WithMember(mongo1);
 
        var exception = Assert.Throws<InvalidOperationException>(action);
        Assert.Contains("key file of its own", exception.Message);
        // NOTE: Rejected before anything was mutated, so the member is still usable.
        Assert.Null(mongo1.Resource.ReplicaSetName);
    }
 
    [Fact]
    public async Task RestartedReplicaSetReportsRunningOnlyAfterMemberIsReady()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var mongo1 = builder.AddMongoDB("mongo1");
        var replicaSet = builder.AddMongoDBReplicaSet("rs0").WithMember(mongo1);
 
        using var app = builder.Build();
        var eventing = app.Services.GetRequiredService<IDistributedApplicationEventing>();
        var notifications = app.Services.GetRequiredService<ResourceNotificationService>();
 
        replicaSet.Resource.IsConfigured = true;
        await notifications.PublishUpdateAsync(replicaSet.Resource, snapshot => snapshot with
        {
            State = KnownResourceStates.Running
        });
 
        var stoppedSnapshot = new CustomResourceSnapshot
        {
            ResourceType = nameof(MongoDBServerResource),
            Properties = [],
            State = KnownResourceStates.Exited
        };
        var stoppedResourceEvent = new ResourceEvent(mongo1.Resource, mongo1.Resource.Name, stoppedSnapshot);
        await eventing.PublishAsync(new ResourceStoppedEvent(mongo1.Resource, app.Services, stoppedResourceEvent));
 
        Assert.Equal(KnownResourceStates.Exited, GetReplicaSetState());
 
        await eventing.PublishAsync(new BeforeResourceStartedEvent(mongo1.Resource, app.Services));
 
        Assert.Equal(KnownResourceStates.Starting, GetReplicaSetState());
 
        using var staleReadyCts = new CancellationTokenSource();
        staleReadyCts.Cancel();
        await eventing.PublishAsync(new ResourceReadyEvent(mongo1.Resource, app.Services), staleReadyCts.Token);
 
        Assert.Equal(KnownResourceStates.Starting, GetReplicaSetState());
 
        await eventing.PublishAsync(new ResourceReadyEvent(mongo1.Resource, app.Services));
 
        Assert.Equal(KnownResourceStates.Running, GetReplicaSetState());
 
        string? GetReplicaSetState()
        {
            Assert.True(notifications.TryGetCurrentState(replicaSet.Resource.Name, out var resourceEvent));
            return resourceEvent.Snapshot.State?.Text;
        }
    }
 
    [Fact]
    public async Task WithMemberKeepsAMemberKeyFileThatAlreadyUsesTheSharedParameter()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var rs = builder.AddMongoDBReplicaSet("rs0");
 
        // NOTE: The same key file the replica set shares, but mounted somewhere other than the default. Configuring it
        // again would throw on the path, after the member had already been mutated.
        var mongo1 = builder.AddMongoDB("mongo1").WithKeyFile(rs.Resource.SharedKeyFileParameter, "/custom/rs.key");
        rs.WithMember(mongo1);
 
        Assert.Equal("rs0", mongo1.Resource.ReplicaSetName);
        var args = await ArgumentEvaluator.GetArgumentListAsync(mongo1.Resource);
        Assert.Equal(1, args.Count(a => a == "--keyFile"));
        Assert.Equal("/custom/rs.key", args[args.IndexOf("--keyFile") + 1]);
    }
 
    [Fact]
    public void WithMemberLeavesTheMemberUntouchedWhenItRejectsTheCredentials()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var memberPassword = builder.AddParameter("member-password", "p@ssw0rd", secret: true);
        var mongo1 = builder.AddMongoDB("mongo1", password: memberPassword);
 
        Assert.Throws<InvalidOperationException>(() => builder.AddMongoDBReplicaSet("rs0").WithMember(mongo1));
 
        // NOTE: The rejected member must not have been left half configured, or the corrected call below would be turned
        // away as a duplicate rather than accepted.
        Assert.Null(mongo1.Resource.ReplicaSetName);
        Assert.False(mongo1.Resource.HasAnnotationOfType<MongoDBServerKeyFileAnnotation>());
 
        var rs = builder.AddMongoDBReplicaSet("rs1", password: memberPassword).WithMember(mongo1);
 
        Assert.Equal("rs1", mongo1.Resource.ReplicaSetName);
        Assert.Single(rs.Resource.Members);
    }
 
    [Fact]
    public void WithMemberAcceptsAMemberThatSharesTheReplicaSetCredentials()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var userName = builder.AddParameter("shared-username", "someone");
        var password = builder.AddParameter("shared-password", "p@ssw0rd", secret: true);
        var mongo1 = builder.AddMongoDB("mongo1", userName: userName, password: password);
 
        var rs = builder.AddMongoDBReplicaSet("rs0", userName: userName, password: password).WithMember(mongo1);
 
        Assert.Same(userName.Resource, mongo1.Resource.UserNameParameter);
        Assert.Same(password.Resource, mongo1.Resource.PasswordParameter);
        Assert.Single(rs.Resource.Members);
    }
 
    [Fact]
    public void WithMemberAdoptsTheReplicaSetCredentialsWhenTheMemberHasGeneratedOnes()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var mongo1 = builder.AddMongoDB("mongo1");
        var rs = builder.AddMongoDBReplicaSet("rs0").WithMember(mongo1);
 
        Assert.Same(rs.Resource.SharedPasswordParameter, mongo1.Resource.PasswordParameter);
    }
 
    [Fact]
    public async Task ReplicaSetExposesConnectionProperties()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var mongo1 = builder.AddMongoDB("mongo1")
            .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 27017));
        var rs = builder.AddMongoDBReplicaSet("rs0").WithMember(mongo1);
 
        var properties = ((IResourceWithConnectionString)rs.Resource).GetConnectionProperties().ToArray();
 
        Assert.Equal(
            ["Username", "Password", "AuthenticationDatabase", "AuthenticationMechanism", "ReplicaSetName", "Uri"],
            properties.Select(p => p.Key));
        Assert.Equal("admin", await properties.Single(p => p.Key == "Username").Value.GetValueAsync(CancellationToken.None));
        Assert.Equal("admin", await properties.Single(p => p.Key == "AuthenticationDatabase").Value.GetValueAsync(CancellationToken.None));
        Assert.Equal("SCRAM-SHA-256", await properties.Single(p => p.Key == "AuthenticationMechanism").Value.GetValueAsync(CancellationToken.None));
        Assert.Equal("rs0", await properties.Single(p => p.Key == "ReplicaSetName").Value.GetValueAsync(CancellationToken.None));
 
        var uri = await properties.Single(p => p.Key == "Uri").Value.GetValueAsync(CancellationToken.None);
        Assert.Equal(await rs.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None), uri);
    }
 
    [Fact]
    public void ReplicaSetResourceThrowsWhenRequiredParametersAreNull()
    {
        var keyFile = new ParameterResource("keyfile", _ => "key");
        var password = new ParameterResource("password", _ => "pass");
 
        Assert.Equal("keyFile", Assert.Throws<ArgumentNullException>(
            () => new MongoDBReplicaSetResource("rs0", null!, null, password)).ParamName);
        Assert.Equal("sharedPassword", Assert.Throws<ArgumentNullException>(
            () => new MongoDBReplicaSetResource("rs0", keyFile, null, null!)).ParamName);
    }
 
    [Fact]
    public void BuildMembersConfigurationAllocatesSequentialIdsForANewReplicaSet()
    {
        var members = BuildMembers(("mongo1:27017", "localhost:27017"), ("mongo2:27017", "localhost:27018"));
 
        var configuration = MongoDBReplicaSetBuilderExtensions.BuildMembersConfiguration(members, currentMembers: null);
 
        Assert.Equal([0, 1], configuration.Select(m => m["_id"].AsInt32));
        Assert.Equal(["mongo1:27017", "mongo2:27017"], configuration.Select(m => m["host"].AsString));
        Assert.Equal(["localhost:27017", "localhost:27018"], configuration.Select(m => m["horizons"]["external"].AsString));
    }
 
    [Fact]
    public void BuildMembersConfigurationPreservesIdsOfExistingMembersWhenAMemberIsAppended()
    {
        var currentMembers = BuildCurrentMembers(("mongo1:27017", 0), ("mongo2:27017", 1));
        var members = BuildMembers(
            ("mongo1:27017", "localhost:27017"),
            ("mongo2:27017", "localhost:27018"),
            ("mongo3:27017", "localhost:27019"));
 
        var configuration = MongoDBReplicaSetBuilderExtensions.BuildMembersConfiguration(members, currentMembers);
 
        Assert.Equal([0, 1, 2], configuration.Select(m => m["_id"].AsInt32));
    }
 
    [Fact]
    public void BuildMembersConfigurationPreservesIdsOfExistingMembersWhenAMemberIsRemoved()
    {
        var currentMembers = BuildCurrentMembers(("mongo1:27017", 0), ("mongo2:27017", 1), ("mongo3:27017", 2));
        // `mongo2` is gone; `mongo1` and `mongo3` must keep the ids they already have.
        var members = BuildMembers(("mongo1:27017", "localhost:27017"), ("mongo3:27017", "localhost:27019"));
 
        var configuration = MongoDBReplicaSetBuilderExtensions.BuildMembersConfiguration(members, currentMembers);
 
        Assert.Equal([0, 2], configuration.Select(m => m["_id"].AsInt32));
        Assert.Equal(["mongo1:27017", "mongo3:27017"], configuration.Select(m => m["host"].AsString));
    }
 
    [Fact]
    public void BuildMembersConfigurationPreservesIdsOfExistingMembersWhenMembersAreReordered()
    {
        var currentMembers = BuildCurrentMembers(("mongo1:27017", 0), ("mongo2:27017", 1), ("mongo3:27017", 2));
        var members = BuildMembers(
            ("mongo3:27017", "localhost:27019"),
            ("mongo1:27017", "localhost:27017"),
            ("mongo2:27017", "localhost:27018"));
 
        var configuration = MongoDBReplicaSetBuilderExtensions.BuildMembersConfiguration(members, currentMembers);
 
        Assert.Equal([2, 0, 1], configuration.Select(m => m["_id"].AsInt32));
    }
 
    [Fact]
    public void BuildMembersConfigurationAllocatesUnusedIdsForNewMembersWhenAMemberIsReplaced()
    {
        var currentMembers = BuildCurrentMembers(("mongo1:27017", 0), ("mongo2:27017", 1));
        // `mongo2` is replaced by `mongo3`, which must not reuse an id that is still taken.
        var members = BuildMembers(("mongo1:27017", "localhost:27017"), ("mongo3:27017", "localhost:27019"));
 
        var configuration = MongoDBReplicaSetBuilderExtensions.BuildMembersConfiguration(members, currentMembers);
 
        Assert.Equal([0, 2], configuration.Select(m => m["_id"].AsInt32));
    }
 
    [Fact]
    public void BuildMembersConfigurationMakesMembersPastTheSeventhNonVoting()
    {
        // NOTE: MongoDB allows at most seven voting members and `replSetInitiate` fails outright when handed more.
        var members = BuildMembers([.. Enumerable.Range(1, 9).Select(i => ($"mongo{i}:27017", $"localhost:2701{i}"))]);
 
        var configuration = MongoDBReplicaSetBuilderExtensions.BuildMembersConfiguration(members, currentMembers: null);
 
        var voting = configuration.OfType<BsonDocument>().Where(m => !m.Contains("votes") || m["votes"].AsInt32 != 0).ToList();
        Assert.Equal(7, voting.Count);
 
        foreach (var nonVoting in configuration.OfType<BsonDocument>().Skip(7))
        {
            Assert.Equal(0, nonVoting["votes"].AsInt32);
            Assert.Equal(0, nonVoting["priority"].AsInt32);
        }
    }
 
    private static MongoDBReplicaSetBuilderExtensions.MemberHosts[] BuildMembers(params (string Internal, string External)[] members) =>
        [.. members.Select(m => new MongoDBReplicaSetBuilderExtensions.MemberHosts(m.Internal, m.External))];
 
    private static BsonArray BuildCurrentMembers(params (string Host, int Id)[] members) =>
        [.. members.Select(m => new BsonDocument { ["_id"] = m.Id, ["host"] = m.Host })];
 
    private static X509Certificate2 CreateTestCertificate()
    {
        using var rsa = RSA.Create(2048);
        var request = new CertificateRequest("CN=test", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
        request.CertificateExtensions.Add(new X509KeyUsageExtension(
            X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, false));
 
        return request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));
    }
}