From 71edfe1a3e7a3e04dbb66fc0288b8514d278ef8b Mon Sep 17 00:00:00 2001 From: Jeff Goddard Date: Wed, 10 Sep 2025 11:30:47 +0100 Subject: [PATCH] Add container for MongoDb Atlas Local --- modules/mongodb/README.rst | 1 + .../testcontainers/mongodb/__init__.py | 92 +++++++++++++++++++ modules/mongodb/tests/test_mongodb.py | 50 +++++++++- 3 files changed, 142 insertions(+), 1 deletion(-) diff --git a/modules/mongodb/README.rst b/modules/mongodb/README.rst index 37e836406..4c2af683e 100644 --- a/modules/mongodb/README.rst +++ b/modules/mongodb/README.rst @@ -1,2 +1,3 @@ .. autoclass:: testcontainers.mongodb.MongoDbContainer +.. autoclass:: testcontainers.mongodb.MongoDBAtlasLocalContainer .. title:: testcontainers.mongodb.MongoDbContainer diff --git a/modules/mongodb/testcontainers/mongodb/__init__.py b/modules/mongodb/testcontainers/mongodb/__init__.py index 4a436b195..6d2d083bf 100644 --- a/modules/mongodb/testcontainers/mongodb/__init__.py +++ b/modules/mongodb/testcontainers/mongodb/__init__.py @@ -17,6 +17,7 @@ from testcontainers.core.generic import DbContainer from testcontainers.core.utils import raise_for_deprecated_parameter +from testcontainers.core.wait_strategies import HealthcheckWaitStrategy from testcontainers.core.waiting_utils import wait_for_logs @@ -81,3 +82,94 @@ def _connect(self) -> None: def get_connection_client(self) -> MongoClient: return MongoClient(self.get_connection_url()) + + +class MongoDBAtlasLocalContainer(DbContainer): + """ + MongoDB Atlas Local document-based database container. + + This is the local version of the Mongo Atlas service. + It includes Mongo DB and Mongo Atlas Search services + Example: + + .. doctest:: + + >>> from testcontainers.mongodb import MongoDBAtlasLocalContainer + >>> import time + >>> with MongoDBAtlasLocalContainer("mongodb/mongodb-atlas-local:8.0.13") as mongo: + ... db = mongo.get_connection_client().test + ... # Insert a database entry + ... result = db.restaurants.insert_one( + ... { + ... "name": "Vella", + ... "cuisine": "Italian", + ... "restaurant_id": "123456" + ... } + ... ) + ... # add an index + ... db.restaurants.create_search_index( + ... { + ... "definition": { + ... "mappings": { + ... "dynamic": True + ... } + ... }, + ... "name": "default" + ... } + ... ) + ... # wait for the index to be created + ... time.sleep(1) + ... + ... # Find the restaurant document + ... result = db.restaurants.aggregate([{ + ... "$search": { + ... "index": "default", + ... "text": { + ... "query": "Vella", + ... "path": "name" + ... } + ... } + ... }]).next() + ... result["restaurant_id"] + '123456' + """ + + def __init__( + self, + image: str = "mongodb/mongodb-atlas-local:latest", + port: int = 27017, + username: Optional[str] = None, + password: Optional[str] = None, + dbname: Optional[str] = None, + **kwargs, + ) -> None: + raise_for_deprecated_parameter(kwargs, "port_to_expose", "port") + super().__init__(image=image, **kwargs) + self.username = username if username else os.environ.get("MONGODB_INITDB_ROOT_USERNAME", "test") + self.password = password if password else os.environ.get("MONGODB_INITDB_ROOT_PASSWORD", "test") + self.dbname = dbname if dbname else os.environ.get("MONGODB_INITDB_DATABASE", "test") + self.port = port + self.with_exposed_ports(self.port) + + def _configure(self) -> None: + self.with_env("MONGODB_INITDB_ROOT_USERNAME", self.username) + self.with_env("MONGODB_INITDB_ROOT_PASSWORD", self.password) + self.with_env("MONGODB_INITDB_DATABASE", self.dbname) + + def get_connection_url(self) -> str: + return ( + self._create_connection_url( + dialect="mongodb", + username=self.username, + password=self.password, + port=self.port, + ) + + "?directConnection=true" + ) + + def _connect(self) -> None: + strategy = HealthcheckWaitStrategy() + strategy.wait_until_ready(self) + + def get_connection_client(self) -> MongoClient: + return MongoClient(self.get_connection_url()) diff --git a/modules/mongodb/tests/test_mongodb.py b/modules/mongodb/tests/test_mongodb.py index 9bf3600f2..352c4a709 100644 --- a/modules/mongodb/tests/test_mongodb.py +++ b/modules/mongodb/tests/test_mongodb.py @@ -1,8 +1,9 @@ +import time import pytest from pymongo import MongoClient from pymongo.errors import OperationFailure -from testcontainers.mongodb import MongoDbContainer +from testcontainers.mongodb import MongoDbContainer, MongoDBAtlasLocalContainer @pytest.mark.parametrize("version", ["7.0.7", "6.0.14", "5.0.26"]) @@ -28,6 +29,53 @@ def test_docker_run_mongodb(version: str): assert cursor.next()["restaurant_id"] == doc["restaurant_id"] +@pytest.mark.parametrize("version", ["8.0.13", "7.0.23"]) +def test_docker_run_mongodb_atlas_local(version: str): + with MongoDBAtlasLocalContainer(f"mongodb/mongodb-atlas-local:{version}") as mongo_atlas: + db = mongo_atlas.get_connection_client().test + index_doc = { + "definition": { + "mappings": { + "dynamic": False, + "fields": { + "borough": {"analyzer": "lucene.keyword", "type": "string"}, + }, + }, + }, + "name": "test", + } + + db.create_collection("restaurants") + + db.restaurants.create_search_index(index_doc) + + doc = { + "address": { + "street": "2 Avenue", + "zipcode": "10075", + "building": "1480", + "coord": [-73.9557413, 40.7720266], + }, + "borough": "Manhattan", + "cuisine": "Italian", + "name": "Vella", + "restaurant_id": "41704620", + } + result = db.restaurants.insert_one(doc) + assert result.inserted_id + + # Wait for index to catch up + indexes = db.restaurants.list_search_indexes() + while indexes.next()["status"] != "READY": + time.sleep(0.1) + indexes = db.restaurants.list_search_indexes() + + cursor = db.restaurants.aggregate( + [{"$search": {"index": "test", "text": {"query": "Manhattan", "path": "borough"}}}] + ) + assert cursor.next()["restaurant_id"] == doc["restaurant_id"] + + # This is a feature in the generic DbContainer class # but it can't be tested on its own # so is tested in various database modules: