Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Testcontainers.dic
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ lipsum
ltsc
memopt
mongosh
mongot
mosquitto
mycounter
mydatabase
Expand Down
1 change: 1 addition & 0 deletions Testcontainers.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=ltsc/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=memopt/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mongosh/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mongot/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mosquitto/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mycounter/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mydatabase/@EntryIndexedValue">True</s:Boolean>
Expand Down
2 changes: 2 additions & 0 deletions Testcontainers.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<Project Path="src/Testcontainers.MariaDb/Testcontainers.MariaDb.csproj"/>
<Project Path="src/Testcontainers.Milvus/Testcontainers.Milvus.csproj"/>
<Project Path="src/Testcontainers.MongoDb/Testcontainers.MongoDb.csproj"/>
<Project Path="src/Testcontainers.MongoDbAtlasLocal/Testcontainers.MongoDbAtlasLocal.csproj"/>
<Project Path="src/Testcontainers.Mosquitto/Testcontainers.Mosquitto.csproj"/>
<Project Path="src/Testcontainers.MsSql/Testcontainers.MsSql.csproj"/>
<Project Path="src/Testcontainers.MySql/Testcontainers.MySql.csproj"/>
Expand Down Expand Up @@ -121,6 +122,7 @@
<Project Path="tests/Testcontainers.MariaDb.Tests/Testcontainers.MariaDb.Tests.csproj"/>
<Project Path="tests/Testcontainers.Milvus.Tests/Testcontainers.Milvus.Tests.csproj"/>
<Project Path="tests/Testcontainers.MongoDb.Tests/Testcontainers.MongoDb.Tests.csproj"/>
<Project Path="tests/Testcontainers.MongoDbAtlasLocal.Tests/Testcontainers.MongoDbAtlasLocal.Tests.csproj"/>
<Project Path="tests/Testcontainers.Mosquitto.Tests/Testcontainers.Mosquitto.Tests.csproj"/>
<Project Path="tests/Testcontainers.MsSql.Tests/Testcontainers.MsSql.Tests.csproj"/>
<Project Path="tests/Testcontainers.MySql.Tests/Testcontainers.MySql.Tests.csproj"/>
Expand Down
1 change: 1 addition & 0 deletions docs/modules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ await moduleNameContainer.StartAsync();
| Milvus | `milvusdb/milvus:v2.3.10` | [NuGet](https://www.nuget.org/packages/Testcontainers.Milvus) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Milvus) |
| MinIO | `minio/minio:RELEASE.2023-01-31T02-24-19Z` | [NuGet](https://www.nuget.org/packages/Testcontainers.Minio) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Minio) |
| MongoDB | `mongo:6.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.MongoDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MongoDb) |
| MongoDB Atlas Local | `mongodb/mongodb-atlas-local:8.0.32` | [NuGet](https://www.nuget.org/packages/Testcontainers.MongoDbAtlasLocal) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MongoDbAtlasLocal) |
| Mosquitto | `eclipse-mosquitto:2.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.Mosquitto) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Mosquitto) |
| MySQL | `mysql:8.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.MySql) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MySql) |
| NATS | `nats:2.9` | [NuGet](https://www.nuget.org/packages/Testcontainers.Nats) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Nats) |
Expand Down
61 changes: 61 additions & 0 deletions docs/modules/mongodb-atlas-local.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# MongoDB Atlas Local

[MongoDB Atlas Local](https://www.mongodb.com/docs/atlas/cli/current/atlas-cli-deploy-docker/) runs a local Atlas deployment in a single container. Next to MongoDB, it includes the Atlas Search process, so features like [Atlas Search](https://www.mongodb.com/docs/atlas/atlas-search/) (`$search`) and [Atlas Vector Search](https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-overview/) (`$vectorSearch`) can be tested without an Atlas cluster.

Add the following dependency to your project file:

```shell title="NuGet"
dotnet add package Testcontainers.MongoDbAtlasLocal
```

You can start a MongoDB Atlas Local container instance from any .NET application. Here, we create different container instances and pass them to the base test class. This allows us to test different configurations. Authentication is disabled by default. Set a username and a password to enable it.

=== "Create Container Instance"
```csharp
--8<-- "tests/Testcontainers.MongoDbAtlasLocal.Tests/MongoDbAtlasLocalContainerTest.cs:CreateMongoDbAtlasLocalContainer"
```

This example uses xUnit.net's `IAsyncLifetime` interface to manage the lifecycle of the container. The container is started in the `InitializeAsync` method before the test method runs, ensuring that the environment is ready for testing. After the test completes, the container is removed in the `DisposeAsync` method.

=== "Usage Example"
```csharp
--8<-- "tests/Testcontainers.MongoDbAtlasLocal.Tests/MongoDbAtlasLocalContainerTest.cs:UseMongoDbAtlasLocalContainer"
```

=== "Atlas Search Helper"
```csharp
--8<-- "tests/Testcontainers.MongoDbAtlasLocal.Tests/AtlasSearch.cs"
```

The test example uses the following NuGet dependencies:

=== "Package References"
```xml
--8<-- "tests/Testcontainers.MongoDbAtlasLocal.Tests/Testcontainers.MongoDbAtlasLocal.Tests.csproj:PackageReferences"
```

To execute the tests, use the command `dotnet test` from a terminal.

--8<-- "docs/modules/_call_out_test_projects.txt"

!!! note

Atlas Search builds indexes asynchronously. A search index becomes queryable shortly after it has been created, and newly written documents become searchable shortly after they have been written. Wait until the index reports `queryable: true` before running search queries, and retry a query until it returns the documents you expect, as the Atlas Search helper above does.

## Seeding the deployment

Init scripts seed the deployment on its first start. `WithInitScript(string)` copies a script file from the test host, and `WithInitScriptContent(string, string)` creates one from a string. JavaScript (`.js`) scripts run in `mongosh` against the database set with `WithInitDatabase(string)` (default `test`), and shell (`.sh`) scripts run in `bash`. The container silently skips files with any other extension, so the builder rejects them. Scripts run in alphabetical order of their file names and finish before the container is reported ready. They can also create Atlas Search indexes. A restarted or reused container keeps its data and does not run the scripts again.

=== "Seed Configuration"
```csharp
--8<-- "tests/Testcontainers.MongoDbAtlasLocal.Tests/MongoDbAtlasLocalSeedTest.cs:SeedMongoDbAtlasLocalContainer"
```

=== "Seed Script"
```javascript
--8<-- "tests/Testcontainers.MongoDbAtlasLocal.Tests/Seed/01-movies.js"
```

## Telemetry

The MongoDB Atlas Local image sends telemetry to MongoDB by default. Call `WithNoTelemetry()` to disable it, as shown in the seed configuration above.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ nav:
- modules/garnet.md
- modules/grafana.md
- modules/mongodb.md
- modules/mongodb-atlas-local.md
- modules/mssql.md
- modules/neo4j.md
- modules/opensearch.md
Expand Down
1 change: 1 addition & 0 deletions src/Testcontainers.MongoDbAtlasLocal/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
225 changes: 225 additions & 0 deletions src/Testcontainers.MongoDbAtlasLocal/MongoDbAtlasLocalBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
namespace Testcontainers.MongoDbAtlasLocal;

/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
[PublicAPI]
public sealed class MongoDbAtlasLocalBuilder : ContainerBuilder<MongoDbAtlasLocalBuilder, MongoDbAtlasLocalContainer, MongoDbAtlasLocalConfiguration>
{
public const ushort MongoDbAtlasLocalPort = 27017;

public const string InitScriptsDirectoryPath = "/docker-entrypoint-initdb.d/";

private static readonly string[] InitScriptFileExtensions = { ".js", ".sh" };

/// <summary>
/// Initializes a new instance of the <see cref="MongoDbAtlasLocalBuilder" /> class.
/// </summary>
/// <param name="image">
/// The full Docker image name, including the image repository and tag
/// (e.g., <c>mongodb/mongodb-atlas-local:8.0.32</c>).
/// </param>
/// <remarks>
/// Docker image tags available at <see href="https://hub.docker.com/r/mongodb/mongodb-atlas-local/tags" />.
/// </remarks>
public MongoDbAtlasLocalBuilder(string image)
: this(new DockerImage(image))
{
}

/// <summary>
/// Initializes a new instance of the <see cref="MongoDbAtlasLocalBuilder" /> class.
/// </summary>
/// <param name="image">
/// An <see cref="IImage" /> instance that specifies the Docker image to be used
/// for the container builder configuration.
/// </param>
/// <remarks>
/// Docker image tags available at <see href="https://hub.docker.com/r/mongodb/mongodb-atlas-local/tags" />.
/// </remarks>
public MongoDbAtlasLocalBuilder(IImage image)
: this(new MongoDbAtlasLocalConfiguration())
{
DockerResourceConfiguration = Init().WithImage(image).DockerResourceConfiguration;
}

/// <summary>
/// Initializes a new instance of the <see cref="MongoDbAtlasLocalBuilder" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
private MongoDbAtlasLocalBuilder(MongoDbAtlasLocalConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
DockerResourceConfiguration = resourceConfiguration;
}

/// <inheritdoc />
protected override MongoDbAtlasLocalConfiguration DockerResourceConfiguration { get; }

/// <summary>
/// Sets the MongoDb Atlas Local username.
/// </summary>
/// <remarks>
/// Authentication is disabled by default. Set both the username and the password
/// to create a root user and enable authentication.
/// </remarks>
/// <param name="username">The MongoDb Atlas Local username.</param>
/// <returns>A configured instance of <see cref="MongoDbAtlasLocalBuilder" />.</returns>
public MongoDbAtlasLocalBuilder WithUsername(string username)
{
var initDbRootUsername = username ?? string.Empty;

return Merge(DockerResourceConfiguration, new MongoDbAtlasLocalConfiguration(username: initDbRootUsername))
.WithEnvironment("MONGODB_INITDB_ROOT_USERNAME", initDbRootUsername);
}

/// <summary>
/// Sets the MongoDb Atlas Local password.
/// </summary>
/// <remarks>
/// Authentication is disabled by default. Set both the username and the password
/// to create a root user and enable authentication.
/// </remarks>
/// <param name="password">The MongoDb Atlas Local password.</param>
/// <returns>A configured instance of <see cref="MongoDbAtlasLocalBuilder" />.</returns>
public MongoDbAtlasLocalBuilder WithPassword(string password)
{
var initDbRootPassword = password ?? string.Empty;

return Merge(DockerResourceConfiguration, new MongoDbAtlasLocalConfiguration(password: initDbRootPassword))
.WithEnvironment("MONGODB_INITDB_ROOT_PASSWORD", initDbRootPassword);
}

/// <summary>
/// Sets the database the JavaScript init scripts run against.
/// </summary>
/// <remarks>
/// Defaults to <c>test</c>. The database is not added to the connection string, because MongoDb
/// would use it as the authentication database, which does not contain the root user.
/// </remarks>
/// <param name="database">The init database.</param>
/// <returns>A configured instance of <see cref="MongoDbAtlasLocalBuilder" />.</returns>
public MongoDbAtlasLocalBuilder WithInitDatabase(string database)
{
return WithEnvironment("MONGODB_INITDB_DATABASE", database);
}

/// <summary>
/// Copies an init script to the container to seed the deployment on its first start.
/// </summary>
/// <remarks>
/// JavaScript (<c>.js</c>) scripts run in <c>mongosh</c> against the init database, shell (<c>.sh</c>)
/// scripts run in <c>bash</c>. Scripts run in alphabetical order of their file names, before the
/// container is reported ready, and only once. A restarted or reused container keeps its data and
/// does not run them again. Init scripts can create Atlas Search indexes.
/// </remarks>
/// <param name="scriptFilePath">The host path of the init script.</param>
/// <returns>A configured instance of <see cref="MongoDbAtlasLocalBuilder" />.</returns>
/// <exception cref="ArgumentException">Thrown when the file name does not end with <c>.js</c> or <c>.sh</c>.</exception>
public MongoDbAtlasLocalBuilder WithInitScript(string scriptFilePath)
{
_ = Guard.Argument(scriptFilePath, nameof(scriptFilePath))
.NotNull()
.NotEmpty();

ValidateInitScriptFileName(Path.GetFileName(scriptFilePath), nameof(scriptFilePath));

return WithResourceMapping(FilePath.Of(scriptFilePath), DirectoryPath.Of(InitScriptsDirectoryPath));
}

/// <summary>
/// Copies an init script to the container to seed the deployment on its first start.
/// </summary>
/// <remarks>
/// See <see cref="WithInitScript(string)" /> for how init scripts run. The file name's extension
/// selects the interpreter, either <c>.js</c> or <c>.sh</c>.
/// </remarks>
/// <param name="fileName">The file name of the init script, e.g. <c>01-seed.js</c>.</param>
/// <param name="scriptContent">The content of the init script.</param>
/// <returns>A configured instance of <see cref="MongoDbAtlasLocalBuilder" />.</returns>
/// <exception cref="ArgumentException">Thrown when the file name contains a path separator or does not end with <c>.js</c> or <c>.sh</c>.</exception>
public MongoDbAtlasLocalBuilder WithInitScriptContent(string fileName, string scriptContent)
{
_ = Guard.Argument(fileName, nameof(fileName))
.NotNull()
.NotEmpty()
.ThrowIf(argument => argument.Value.IndexOfAny(new[] { '/', '\\' }) >= 0, argument => new ArgumentException("The init script file name must not contain a path separator.", argument.Name));

_ = Guard.Argument(scriptContent, nameof(scriptContent))
.NotNull();

ValidateInitScriptFileName(fileName, nameof(fileName));

return WithResourceMapping(Encoding.UTF8.GetBytes(scriptContent), FilePath.Of(InitScriptsDirectoryPath + fileName));
}

/// <summary>
/// Disables the telemetry the MongoDb Atlas Local container sends to MongoDb.
/// </summary>
/// <returns>A configured instance of <see cref="MongoDbAtlasLocalBuilder" />.</returns>
public MongoDbAtlasLocalBuilder WithNoTelemetry()
{
return WithEnvironment("DO_NOT_TRACK", "1");
}

/// <inheritdoc />
public override MongoDbAtlasLocalContainer Build()
{
Validate();
return new MongoDbAtlasLocalContainer(DockerResourceConfiguration);
}

/// <inheritdoc />
protected override MongoDbAtlasLocalBuilder Init()
{
// The runner's healthcheck checks mongod, the replica set, mongot (the Atlas Search
// process) and that the init scripts have finished. We run it directly instead of
// waiting for the image's HEALTHCHECK, whose first probe runs 30 seconds after start.
return base.Init()
.WithPortBinding(MongoDbAtlasLocalPort, true)
.WithConnectionStringProvider(new MongoDbAtlasLocalConnectionStringProvider())
.WithWaitStrategy(Wait.ForUnixContainer().UntilCommandIsCompleted("runner", "healthcheck"));
}

/// <inheritdoc />
protected override void Validate()
{
const string message = "Missing username or password. Both must be specified for a user to be created.";

base.Validate();

_ = Guard.Argument(DockerResourceConfiguration, "Credentials")
.ThrowIf(argument => 1.Equals(new[] { argument.Value.Username, argument.Value.Password }.Count(string.IsNullOrWhiteSpace)), argument => new ArgumentException(message, argument.Name));
}

/// <summary>
/// Validates that the runner executes the init script.
/// </summary>
/// <remarks>
/// The runner only executes files ending with <c>.js</c> or <c>.sh</c> (case-sensitive)
/// and silently skips any other file, e.g. <c>seed.json</c> or <c>seed.JS</c>.
/// </remarks>
/// <param name="fileName">The file name of the init script.</param>
/// <param name="parameterName">The name of the validated parameter.</param>
private static void ValidateInitScriptFileName(string fileName, string parameterName)
{
_ = Guard.Argument(fileName, parameterName)
.ThrowIf(argument => !InitScriptFileExtensions.Any(fileExtension => argument.Value.EndsWith(fileExtension, StringComparison.Ordinal)), argument => new ArgumentException("The init script file name must end with .js or .sh, the container skips any other file.", argument.Name));
}

/// <inheritdoc />
protected override MongoDbAtlasLocalBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new MongoDbAtlasLocalConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override MongoDbAtlasLocalBuilder Clone(IContainerConfiguration resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new MongoDbAtlasLocalConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override MongoDbAtlasLocalBuilder Merge(MongoDbAtlasLocalConfiguration oldValue, MongoDbAtlasLocalConfiguration newValue)
{
return new MongoDbAtlasLocalBuilder(new MongoDbAtlasLocalConfiguration(oldValue, newValue));
}
}
Loading
Loading