diff --git a/docs/modules/mongodb.md b/docs/modules/mongodb.md index 478d0d63e..7ebbdc6b1 100644 --- a/docs/modules/mongodb.md +++ b/docs/modules/mongodb.md @@ -45,3 +45,11 @@ public sealed class MongoDbContainerTest : IAsyncLifetime ``` To execute the tests, use the command `dotnet test` from a terminal. + +## MongoDb Replica Set + +By default, MongoDB runs as a standalone instance. If your tests require a MongoDB replica set, use the code below which will initialize it as a single-node replica set: + +```csharp +MongoDbContainer _mongoDbContainer = new MongoDbBuilder().WithReplicaSet().Build(); +``` diff --git a/src/Testcontainers.MongoDb/MongoDbBuilder.cs b/src/Testcontainers.MongoDb/MongoDbBuilder.cs index b4221b018..1a806c7a8 100644 --- a/src/Testcontainers.MongoDb/MongoDbBuilder.cs +++ b/src/Testcontainers.MongoDb/MongoDbBuilder.cs @@ -12,6 +12,10 @@ public sealed class MongoDbBuilder : ContainerBuilder /// Initializes a new instance of the class. /// @@ -60,15 +64,44 @@ public MongoDbBuilder WithPassword(string password) .WithEnvironment("MONGO_INITDB_ROOT_PASSWORD", initDbRootPassword); } + /// + /// Initialize MongoDB as a single-node replica set. + /// + /// The replica set name. + /// A configured instance of . + public MongoDbBuilder WithReplicaSet(string replicaSetName = "rs0") + { + var initKeyFileScript = new StringWriter(); + initKeyFileScript.NewLine = "\n"; + initKeyFileScript.WriteLine("#!/bin/bash"); + initKeyFileScript.WriteLine("openssl rand -base64 32 > \"" + KeyFileFilePath + "\""); + initKeyFileScript.WriteLine("chmod 600 \"" + KeyFileFilePath + "\""); + + return Merge(DockerResourceConfiguration, new MongoDbConfiguration(replicaSetName: replicaSetName)) + .WithCommand("--replSet", replicaSetName, "--keyFile", KeyFileFilePath, "--bind_ip_all") + .WithResourceMapping(Encoding.Default.GetBytes(initKeyFileScript.ToString()), InitKeyFileScriptFilePath, Unix.FileMode755); + } + /// public override MongoDbContainer Build() { Validate(); - // The wait strategy relies on the configuration of MongoDb. If credentials are - // provided, the log message "Waiting for connections" appears twice. + IWaitUntil waitUntil; + + if (string.IsNullOrEmpty(DockerResourceConfiguration.ReplicaSetName)) + { + // The wait strategy relies on the configuration of MongoDb. If credentials are + // provided, the log message "Waiting for connections" appears twice. + waitUntil = new WaitIndicateReadiness(DockerResourceConfiguration); + } + else + { + waitUntil = new WaitInitiateReplicaSet(DockerResourceConfiguration); + } + // If the user does not provide a custom waiting strategy, append the default MongoDb waiting strategy. - var mongoDbBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil(DockerResourceConfiguration))); + var mongoDbBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(waitUntil)); return new MongoDbContainer(mongoDbBuilder.DockerResourceConfiguration); } @@ -118,17 +151,17 @@ protected override MongoDbBuilder Merge(MongoDbConfiguration oldValue, MongoDbCo } /// - private sealed class WaitUntil : IWaitUntil + private sealed class WaitIndicateReadiness : IWaitUntil { private static readonly string[] LineEndings = { "\r\n", "\n" }; private readonly int _count; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The container configuration. - public WaitUntil(MongoDbConfiguration configuration) + public WaitIndicateReadiness(MongoDbConfiguration configuration) { _count = string.IsNullOrEmpty(configuration.Username) && string.IsNullOrEmpty(configuration.Password) ? 1 : 2; } @@ -145,4 +178,34 @@ public async Task UntilAsync(IContainer container) .Count(line => line.Contains("Waiting for connections"))); } } + + /// + private sealed class WaitInitiateReplicaSet : IWaitUntil + { + private readonly string _scriptContent; + + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + public WaitInitiateReplicaSet(MongoDbConfiguration configuration) + { + _scriptContent = $"try{{rs.status().ok}}catch(e){{rs.initiate({{'_id':'{configuration.ReplicaSetName}',members:[{{'_id':1,'host':'127.0.0.1:27017'}}]}}).ok}}"; + } + + /// + public Task UntilAsync(IContainer container) + { + return UntilAsync(container as MongoDbContainer); + } + + /// + private async Task UntilAsync(MongoDbContainer container) + { + var execResult = await container.ExecScriptAsync(_scriptContent) + .ConfigureAwait(false); + + return 0L.Equals(execResult.ExitCode); + } + } } \ No newline at end of file diff --git a/src/Testcontainers.MongoDb/MongoDbConfiguration.cs b/src/Testcontainers.MongoDb/MongoDbConfiguration.cs index b0f008ee6..b069b2959 100644 --- a/src/Testcontainers.MongoDb/MongoDbConfiguration.cs +++ b/src/Testcontainers.MongoDb/MongoDbConfiguration.cs @@ -9,12 +9,15 @@ public sealed class MongoDbConfiguration : ContainerConfiguration /// /// The MongoDb username. /// The MongoDb password. + /// The replica set name. public MongoDbConfiguration( string username = null, - string password = null) + string password = null, + string replicaSetName = null) { Username = username; Password = password; + ReplicaSetName = replicaSetName; } /// @@ -57,6 +60,7 @@ public MongoDbConfiguration(MongoDbConfiguration oldValue, MongoDbConfiguration { Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username); Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password); + ReplicaSetName = BuildConfiguration.Combine(oldValue.ReplicaSetName, newValue.ReplicaSetName); } /// @@ -68,4 +72,12 @@ public MongoDbConfiguration(MongoDbConfiguration oldValue, MongoDbConfiguration /// Gets the MongoDb password. /// public string Password { get; } + + /// + /// Gets the replica set name. + /// + /// + /// If specified, the container will be started as a single-node replica set. + /// + public string ReplicaSetName { get; } } \ No newline at end of file diff --git a/src/Testcontainers/Images/DockerImage.cs b/src/Testcontainers/Images/DockerImage.cs index ef7341c44..8347b3fbe 100644 --- a/src/Testcontainers/Images/DockerImage.cs +++ b/src/Testcontainers/Images/DockerImage.cs @@ -126,7 +126,7 @@ public bool MatchVersion(Predicate predicate) /// public bool MatchVersion(Predicate predicate) { - var versionMatch = Regex.Match(Tag, @"^(\d+)(\.\d+)?(\.\d+)?", RegexOptions.None, TimeSpan.FromSeconds(1)); + var versionMatch = Regex.Match(Tag, "^(\\d+)(\\.\\d+)?(\\.\\d+)?", RegexOptions.None, TimeSpan.FromSeconds(1)); if (!versionMatch.Success) { diff --git a/tests/Testcontainers.MongoDb.Tests/MongoDbContainerTest.cs b/tests/Testcontainers.MongoDb.Tests/MongoDbContainerTest.cs index 732ab150f..81cfa2e46 100644 --- a/tests/Testcontainers.MongoDb.Tests/MongoDbContainerTest.cs +++ b/tests/Testcontainers.MongoDb.Tests/MongoDbContainerTest.cs @@ -4,9 +4,12 @@ public abstract class MongoDbContainerTest : IAsyncLifetime { private readonly MongoDbContainer _mongoDbContainer; - private MongoDbContainerTest(MongoDbContainer mongoDbContainer) + private readonly bool _replicaSetEnabled; + + private MongoDbContainerTest(MongoDbContainer mongoDbContainer, bool replicaSetEnabled = false) { _mongoDbContainer = mongoDbContainer; + _replicaSetEnabled = replicaSetEnabled; } public Task InitializeAsync() @@ -49,6 +52,30 @@ public async Task ExecScriptReturnsSuccessful() Assert.Empty(execResult.Stderr); } + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task ReplicaSetStatus() + { + // Given + const string scriptContent = "rs.status().ok;"; + + // When + var execResult = await _mongoDbContainer.ExecScriptAsync(scriptContent) + .ConfigureAwait(true); + + // Then + if (_replicaSetEnabled) + { + Assert.True(0L.Equals(execResult.ExitCode), execResult.Stderr); + Assert.Empty(execResult.Stderr); + } + else + { + Assert.Equal(1L, execResult.ExitCode); + Assert.Equal("MongoServerError: not running with --replSet\n", execResult.Stderr); + } + } + [UsedImplicitly] public sealed class MongoDbDefaultConfiguration : MongoDbContainerTest { @@ -80,7 +107,25 @@ public MongoDbV5Configuration() public sealed class MongoDbV4Configuration : MongoDbContainerTest { public MongoDbV4Configuration() - : base(new MongoDbBuilder().WithImage("mongo:4.4").Build()) + : base(new MongoDbBuilder().WithImage("mongo:4.4").Build(), true /* Replica set status returns "ok" in MongoDB 4.4 without initialization. */) + { + } + } + + [UsedImplicitly] + public sealed class MongoDbReplicaSetDefaultConfiguration : MongoDbContainerTest + { + public MongoDbReplicaSetDefaultConfiguration() + : base(new MongoDbBuilder().WithReplicaSet().Build(), true) + { + } + } + + [UsedImplicitly] + public sealed class MongoDbNamedReplicaSetConfiguration : MongoDbContainerTest + { + public MongoDbNamedReplicaSetConfiguration() + : base(new MongoDbBuilder().WithReplicaSet("rs1").Build(), true) { } }