// 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 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()); Assert.Null(endpoint.Port); Assert.Equal(27017, endpoint.TargetPort); Assert.True(endpoint.IsProxied); Assert.Single(mongo.Resource.Annotations.OfType()); Assert.True(Assert.IsType( Assert.Single(mongo.Resource.Annotations.OfType()).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()).Value); Assert.Equal(["--keyFile", "/etc/custom.key", "--bind_ip_all", "--replSet", "rs0"], await ArgumentEvaluator.GetArgumentListAsync(mongo.Resource)); Assert.Equal(["key"], builder.Resources.OfType().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(() => 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(() => 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(() => 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(() => 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(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())); 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(() => 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(() => MongoDBSingleMemberReplicaSet.InitializeAndWaitForPrimaryAsync(client.GetDatabase("admin"), "mongo", "localhost:27017", cancellation.Token)); } }