File: SingleMemberReplicaSetTests.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 Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using MongoDB.Bson;
using MongoDB.Driver;
 
#pragma warning disable ASPIRECERTIFICATES001
#pragma warning disable ASPIREMONGODB001
#pragma warning disable ASPIREDOCKERFILEBUILDER001
 
namespace Aspire.Hosting.MongoDB.Tests;
 
public class SingleMemberReplicaSetTests(ITestOutputHelper testOutputHelper)
{
    [Fact]
    public async Task WithReplicaSetKeepsTheServerAndDatabaseModel()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var mongo = builder.AddMongoDB("mongo");
        var password = mongo.Resource.PasswordParameter;
        var database = mongo.AddDatabase("orders");
 
        Assert.Same(mongo, mongo.WithReplicaSet().WithReplicaSet("mongo").WithReplicaSet());
        Assert.Equal("mongo", mongo.Resource.ReplicaSetName);
        Assert.Same(password, mongo.Resource.PasswordParameter);
        Assert.Same(mongo.Resource, database.Resource.Parent);
        Assert.Equal(["mongo", "orders"], builder.Resources.Where(r => r is not ParameterResource).Select(r => r.Name));
        Assert.Equal(["--keyFile", "/etc/rs.key", "--bind_ip_all", "--replSet", "mongo"],
            await ArgumentEvaluator.GetArgumentListAsync(mongo.Resource));
        var endpoint = Assert.Single(mongo.Resource.Annotations.OfType<EndpointAnnotation>());
        Assert.Null(endpoint.Port);
        Assert.Equal(27017, endpoint.TargetPort);
        Assert.True(endpoint.IsProxied);
        Assert.Single(mongo.Resource.Annotations.OfType<MongoDBSingleMemberReplicaSetAnnotation>());
        Assert.True(Assert.IsType<ParameterResource>(
            Assert.Single(mongo.Resource.Annotations.OfType<MongoDBServerKeyFileAnnotation>()).Value).Secret);
    }
 
    [Fact]
    public async Task WithReplicaSetHonorsAnExplicitKeyFileAndSetName()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var key = builder.AddParameter("key", "sharedkey", secret: true);
        var mongo = builder.AddMongoDB("mongo").WithKeyFile(key.Resource, "/etc/custom.key").WithReplicaSet("rs0").WithReplicaSet();
 
        Assert.Equal("rs0", mongo.Resource.ReplicaSetName);
        Assert.Same(key.Resource, Assert.Single(mongo.Resource.Annotations.OfType<MongoDBServerKeyFileAnnotation>()).Value);
        Assert.Equal(["--keyFile", "/etc/custom.key", "--bind_ip_all", "--replSet", "rs0"],
            await ArgumentEvaluator.GetArgumentListAsync(mongo.Resource));
        Assert.Equal(["key"], builder.Resources.OfType<ParameterResource>().Select(r => r.Name));
    }
 
    [Fact]
    public void ReplicaSetNamesAreCaseSensitive()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var mongo = builder.AddMongoDB("mongo").WithReplicaSet("rs0");
        var annotations = mongo.Resource.Annotations.ToArray();
 
        Assert.Throws<InvalidOperationException>(() => mongo.WithReplicaSet("RS0"));
        Assert.Equal(annotations, mongo.Resource.Annotations);
        Assert.Equal("rs0", mongo.Resource.ReplicaSetName);
    }
 
    [Theory]
    [InlineData("rs0")]
    [InlineData("advanced")]
    public void AdvancedSetCannotAdoptASingleMember(string name)
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var mongo = builder.AddMongoDB("mongo").WithReplicaSet("rs0");
        var advanced = builder.AddMongoDBReplicaSet(name);
        var annotations = mongo.Resource.Annotations.ToArray();
        var password = mongo.Resource.PasswordParameter;
 
        var exception = Assert.Throws<InvalidOperationException>(() => advanced.WithMember(mongo));
        Assert.Contains("Remove WithReplicaSet", exception.Message);
        Assert.Equal(annotations, mongo.Resource.Annotations);
        Assert.Same(password, mongo.Resource.PasswordParameter);
        Assert.Empty(advanced.Resource.Members);
    }
 
    [Fact]
    public void SingleMemberCannotInitializeAnAdvancedMember()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        var mongo = builder.AddMongoDB("mongo");
        var advanced = builder.AddMongoDBReplicaSet("advanced").WithMember(mongo);
        var annotations = mongo.Resource.Annotations.ToArray();
 
        Assert.Throws<InvalidOperationException>(() => mongo.WithReplicaSet());
        Assert.Equal(annotations, mongo.Resource.Annotations);
        Assert.Same(mongo.Resource, Assert.Single(advanced.Resource.Members));
    }
 
    [Fact]
    public void DefaultReplicaSetThrowsBeforeMutatingInPublishMode()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var mongo = builder.AddMongoDB("mongo");
        var annotations = mongo.Resource.Annotations.ToArray();
        var resources = builder.Resources.ToArray();
 
        Assert.Throws<NotSupportedException>(() => mongo.WithReplicaSet());
        Assert.Equal(annotations, mongo.Resource.Annotations);
        Assert.Equal(resources, builder.Resources);
    }
 
    [Fact]
    public async Task SingleMemberWithoutDeveloperCertificateKeepsSecureAuthenticationAndDirectPrimaryConnections()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
        builder.Services.AddSingleton<IDeveloperCertificateService>(new TestDeveloperCertificateService(
            [], supportsContainerTrust: true, trustCertificate: true, tlsTerminate: false));
        var password = builder.AddParameter("password", "p@ssword", secret: true);
        var mongo = builder.AddMongoDB("mongo", password: password).WithReplicaSet()
            .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 43210));
        var database = mongo.AddDatabase("orders");
 
        using var app = builder.Build();
        await builder.Eventing.PublishAsync(new BeforeStartEvent(app.Services, app.Services.GetRequiredService<DistributedApplicationModel>()));
 
        Assert.False(mongo.Resource.TlsEnabled);
        Assert.False(mongo.Resource.TlsAllowInvalidCertificates);
        Assert.Equal(["--keyFile", "/etc/rs.key", "--bind_ip_all", "--replSet", "mongo"],
            await ArgumentEvaluator.GetArgumentListAsync(mongo.Resource));
 
        foreach (IResourceWithConnectionString resource in new IResourceWithConnectionString[] { mongo.Resource, database.Resource })
        {
            var connectionString = await resource.GetConnectionStringAsync();
            var url = new MongoUrl(connectionString);
            Assert.True(url.DirectConnection);
            Assert.Equal(ReadPreference.Primary, MongoClientSettings.FromUrl(url).ReadPreference);
            Assert.Equal("admin", url.Username);
            Assert.Equal("p@ssword", url.Password);
            Assert.Equal("localhost:43210", url.Server.ToString());
            Assert.Equal(resource == database.Resource ? "orders" : null, url.DatabaseName);
            Assert.Equal(connectionString, await resource.GetConnectionProperties().Single(p => p.Key == "Uri").Value.GetValueAsync(default));
        }
    }
 
    [Theory]
    [InlineData(null)]
    [InlineData("A resource-specific initialization failure.")]
    public async Task HealthIsUnhealthyBeforeInitializationOrAfterFailure(string? error)
    {
        using var client = new MongoClient();
        var annotation = new MongoDBSingleMemberReplicaSetAnnotation("mongo");
        if (error is not null)
        {
            annotation.InitializationError = error;
        }
 
        var check = new MyMongoDbHealthCheck(client, "admin", annotation);
        var result = await check.CheckHealthAsync(new HealthCheckContext());
        Assert.Equal(HealthStatus.Unhealthy, result.Status);
        Assert.Equal(error ?? "The single-member replica set has not been initialized.", result.Description);
    }
 
    [Fact]
    public void ExistingSingleMemberConfigurationIsNotMutated()
    {
        var config = BsonDocument.Parse("""{ "_id": "mongo", "version": 7, "members": [{ "_id": 0, "host": "localhost:27017", "priority": 1 }], "settings": { "heartbeatIntervalMillis": 2000 } }""");
        var original = config.DeepClone();
 
        MongoDBSingleMemberReplicaSet.ValidateConfiguration(config, "mongo", "localhost:27017");
 
        Assert.Equal(original, config);
    }
 
    [Theory]
    [InlineData("""{ "_id": "other", "members": [{ "host": "localhost:27017" }] }""")]
    [InlineData("""{ "_id": "mongo", "members": [{ "host": "old-host:27017" }] }""")]
    [InlineData("""{ "_id": "mongo", "members": [{ "host": "localhost:27017" }, { "host": "other:27017" }] }""")]
    public void IncompatibleDataIsRejectedWithoutReconfiguration(string json)
    {
        var config = BsonDocument.Parse(json);
        var original = config.DeepClone();
 
        var exception = Assert.Throws<DistributedApplicationException>(() =>
            MongoDBSingleMemberReplicaSet.ValidateConfiguration(config, "mongo", "localhost:27017"));
 
        Assert.Contains("automatic migration or reconfiguration is not supported", exception.Message);
        Assert.Equal(original, config);
    }
 
    [Theory]
    [InlineData("mongo", 1, 1, true)]
    [InlineData("mongo", 2, 1, false)]
    [InlineData("other", 1, 1, false)]
    [InlineData("mongo", 1, 2, false)]
    public void ReadinessRequiresTheSingleMemberPrimary(string name, int state, int memberCount, bool expected)
    {
        var status = new BsonDocument
        {
            ["set"] = name,
            ["myState"] = state,
            ["members"] = new BsonArray(Enumerable.Range(0, memberCount).Select(id => new BsonDocument("_id", id))),
        };
 
        Assert.Equal(expected, MongoDBSingleMemberReplicaSet.IsPrimary(status, "mongo"));
    }
 
    [Fact]
    public async Task InitializationHonorsCancellationBeforeSendingCommands()
    {
        using var client = new MongoClient();
        using var cancellation = new CancellationTokenSource();
        cancellation.Cancel();
 
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
            MongoDBSingleMemberReplicaSet.InitializeAndWaitForPrimaryAsync(client.GetDatabase("admin"), "mongo", "localhost:27017", cancellation.Token));
    }
}