File: ReplicaSet\MongoDBReplicaSetInitiationTests.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.Net;
using System.Reflection;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Core.Clusters;
using MongoDB.Driver.Core.Connections;
using MongoDB.Driver.Core.Servers;
 
#pragma warning disable ASPIREMONGODB001
 
namespace Aspire.Hosting.MongoDB.Tests;
 
public class MongoDBReplicaSetInitiationTests
{
    private static readonly BsonArray s_members =
    [
        new BsonDocument { ["_id"] = 0, ["host"] = "mongo1:27017" },
        new BsonDocument { ["_id"] = 1, ["host"] = "mongo2:27017" },
        new BsonDocument { ["_id"] = 2, ["host"] = "mongo3:27017" },
    ];
 
    [Fact]
    public async Task InitiationWaitsForTheInitialMemberToBecomeWritablePrimaryBeforeReconfiguring()
    {
        // The initiated member keeps reporting that it is not yet a writable primary for a few probes, and interrupts a
        // reconfiguration sent before then with `InterruptedDueToReplStateChange`, like a real `mongod` stepping up does.
        var admin = SteppingUpMember.Create(probesBeforePrimary: 3);
 
        await MongoDBReplicaSetBuilderExtensions.InitiateReplicaSetAsync(admin, "rs0", s_members, CancellationToken.None);
 
        var member = (SteppingUpMember)(object)admin;
        Assert.Equal(["replSetInitiate", "hello", "hello", "hello", "replSetReconfig"], member.Commands.Select(c => c.GetElement(0).Name));
 
        var initiate = member.Commands[0]["replSetInitiate"].AsBsonDocument;
        Assert.Equal("rs0", initiate["_id"].AsString);
        Assert.Equal(new BsonArray([s_members[0]]), initiate["members"].AsBsonArray);
 
        var reconfig = member.Commands[^1];
        Assert.True(reconfig["force"].AsBoolean);
        Assert.Equal(2, reconfig["replSetReconfig"]["version"].AsInt32);
        Assert.Equal(s_members, reconfig["replSetReconfig"]["members"].AsBsonArray);
    }
 
    [Fact]
    public async Task InitiationStopsWaitingForPrimaryWhenCancelled()
    {
        using var cancellation = new CancellationTokenSource();
        var admin = SteppingUpMember.Create(probesBeforePrimary: int.MaxValue, onProbe: probe =>
        {
            if (probe == 2)
            {
                cancellation.Cancel();
            }
        });
 
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
            MongoDBReplicaSetBuilderExtensions.InitiateReplicaSetAsync(admin, "rs0", s_members, cancellation.Token));
 
        var member = (SteppingUpMember)(object)admin;
        Assert.DoesNotContain(member.Commands, c => c.GetElement(0).Name == "replSetReconfig");
    }
 
    public class SteppingUpMember : DispatchProxy
    {
        private int _probesBeforePrimary;
        private int _probes;
        private bool _initiated;
        private Action<int>? _onProbe;
 
        public List<BsonDocument> Commands { get; } = [];
 
        public static IMongoDatabase Create(int probesBeforePrimary, Action<int>? onProbe = null)
        {
            var admin = Create<IMongoDatabase, SteppingUpMember>();
            var member = (SteppingUpMember)(object)admin;
            member._probesBeforePrimary = probesBeforePrimary;
            member._onProbe = onProbe;
            return admin;
        }
 
        private bool IsWritablePrimary => _initiated && _probes >= _probesBeforePrimary;
 
        protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
        {
            if (targetMethod is not { Name: nameof(IMongoDatabase.RunCommandAsync) } || args is not [BsonDocumentCommand<BsonDocument> command, _, CancellationToken cancellationToken])
            {
                throw new NotSupportedException($"Unexpected call to '{targetMethod?.Name}'.");
            }
 
            cancellationToken.ThrowIfCancellationRequested();
            Commands.Add(command.Document);
 
            switch (command.Document.GetElement(0).Name)
            {
                case "replSetInitiate":
                    _initiated = true;
                    return Task.FromResult(new BsonDocument("ok", 1));
 
                case "hello":
                    _probes++;
                    _onProbe?.Invoke(_probes);
                    return Task.FromResult(new BsonDocument { ["isWritablePrimary"] = IsWritablePrimary, ["ok"] = 1 });
 
                case "replSetReconfig" when !IsWritablePrimary:
                    var result = new BsonDocument { ["ok"] = 0, ["code"] = 11602, ["codeName"] = "InterruptedDueToReplStateChange" };
                    var connectionId = new ConnectionId(new ServerId(new ClusterId(), new DnsEndPoint("mongo1", 27017)));
                    return Task.FromException<BsonDocument>(new MongoNodeIsRecoveringException(connectionId, command.Document, result));
 
                case "replSetReconfig":
                    return Task.FromResult(new BsonDocument("ok", 1));
 
                default:
                    throw new NotSupportedException($"Unexpected command '{command.Document}'.");
            }
        }
    }
}