testcontainers_modules/mongo/
mod.rs

1use testcontainers::{
2    core::{CmdWaitFor, ExecCommand, WaitFor},
3    Image,
4};
5
6const NAME: &str = "mongo";
7const TAG: &str = "5.0.6";
8
9/// Type of MongoDB instance to create.
10#[derive(Default, Debug, Clone)]
11enum InstanceKind {
12    /// A standalone MongoDB instance (default).
13    #[default]
14    Standalone,
15    /// A MongoDB replica set instance.
16    ReplSet,
17}
18
19/// Module to work with [`MongoDB`] inside of tests.
20///
21/// Starts an instance of MongoDB based on the official [`MongoDB docker image`].
22///
23/// This module supports both standalone and replica set configurations.
24/// The container exposes port `27017` by default.
25///
26/// # Example
27/// ```
28/// use testcontainers_modules::{mongo::Mongo, testcontainers::runners::SyncRunner};
29///
30/// let mongo_instance = Mongo::default().start().unwrap();
31/// let host = mongo_instance.get_host().unwrap();
32/// let port = mongo_instance.get_host_port_ipv4(27017).unwrap();
33///
34/// // Connect to MongoDB at mongodb://{host}:{port}
35/// ```
36///
37/// [`MongoDB`]: https://www.mongodb.com/
38/// [`MongoDB docker image`]: https://hub.docker.com/_/mongo
39#[derive(Default, Debug, Clone)]
40pub struct Mongo {
41    kind: InstanceKind,
42}
43
44impl Mongo {
45    /// Creates a new standalone MongoDB instance.
46    ///
47    /// This is equivalent to using `Mongo::default()`.
48    ///
49    /// # Example
50    /// ```
51    /// use testcontainers_modules::mongo::Mongo;
52    ///
53    /// let mongo = Mongo::new();
54    /// ```
55    pub fn new() -> Self {
56        Self {
57            kind: InstanceKind::Standalone,
58        }
59    }
60    /// Creates a new MongoDB replica set instance.
61    ///
62    /// This configures MongoDB to run as a replica set, which is useful for testing
63    /// replica set specific features like transactions.
64    ///
65    /// # Example
66    /// ```
67    /// use testcontainers_modules::mongo::Mongo;
68    ///
69    /// let mongo = Mongo::repl_set();
70    /// ```
71    pub fn repl_set() -> Self {
72        Self {
73            kind: InstanceKind::ReplSet,
74        }
75    }
76}
77
78impl Image for Mongo {
79    fn name(&self) -> &str {
80        NAME
81    }
82
83    fn tag(&self) -> &str {
84        TAG
85    }
86
87    fn ready_conditions(&self) -> Vec<WaitFor> {
88        vec![WaitFor::message_on_stdout("Waiting for connections")]
89    }
90
91    fn cmd(&self) -> impl IntoIterator<Item = impl Into<std::borrow::Cow<'_, str>>> {
92        match self.kind {
93            InstanceKind::Standalone => Vec::<String>::new(),
94            InstanceKind::ReplSet => vec!["--replSet".to_string(), "rs".to_string()],
95        }
96    }
97
98    fn exec_after_start(
99        &self,
100        _: testcontainers::core::ContainerState,
101    ) -> Result<Vec<ExecCommand>, testcontainers::TestcontainersError> {
102        match self.kind {
103            InstanceKind::Standalone => Ok(Default::default()),
104            InstanceKind::ReplSet => Ok(vec![ExecCommand::new(vec![
105                "mongosh".to_string(),
106                "--quiet".to_string(),
107                "--eval".to_string(),
108                "'rs.initiate()'".to_string(),
109            ])
110            .with_cmd_ready_condition(CmdWaitFor::message_on_stdout(
111                "Using a default configuration for the set",
112            ))
113            .with_container_ready_conditions(vec![WaitFor::message_on_stdout(
114                "Rebuilding PrimaryOnlyService due to stepUp",
115            )])]),
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use mongodb::*;
123    use testcontainers::{core::IntoContainerPort, runners::AsyncRunner};
124
125    use crate::mongo;
126
127    #[tokio::test]
128    async fn mongo_fetch_document() -> Result<(), Box<dyn std::error::Error + 'static>> {
129        let _ = pretty_env_logger::try_init();
130        let node = mongo::Mongo::default().start().await?;
131        let host_ip = node.get_host().await?;
132        let host_port = node.get_host_port_ipv4(27017.tcp()).await?;
133        let url = format!("mongodb://{host_ip}:{host_port}/");
134
135        let client: Client = Client::with_uri_str(&url).await.unwrap();
136        let db = client.database("some_db");
137        let coll = db.collection("some_coll");
138
139        let insert_one_result = coll.insert_one(bson::doc! { "x": 42 }).await.unwrap();
140        assert!(!insert_one_result
141            .inserted_id
142            .as_object_id()
143            .unwrap()
144            .to_hex()
145            .is_empty());
146
147        let find_one_result: bson::Document = coll
148            .find_one(bson::doc! { "x": 42 })
149            .await
150            .unwrap()
151            .unwrap();
152        assert_eq!(42, find_one_result.get_i32("x").unwrap());
153
154        Ok(())
155    }
156
157    #[tokio::test]
158    async fn mongo_repl_set_fetch_document() -> Result<(), Box<dyn std::error::Error + 'static>> {
159        let _ = pretty_env_logger::try_init();
160        let node = mongo::Mongo::repl_set().start().await?;
161        let host_ip = node.get_host().await?;
162        let host_port = node.get_host_port_ipv4(27017).await?;
163        let url = format!("mongodb://{host_ip}:{host_port}/?directConnection=true",);
164
165        let client: Client = Client::with_uri_str(url).await?;
166        let db = client.database("some_db");
167        let coll = db.collection("some-coll");
168
169        let mut session = client.start_session().await?;
170        session.start_transaction().await?;
171
172        let insert_one_result = coll
173            .insert_one(bson::doc! { "x": 42 })
174            .session(&mut session)
175            .await?;
176        assert!(!insert_one_result
177            .inserted_id
178            .as_object_id()
179            .unwrap()
180            .to_hex()
181            .is_empty());
182        session.commit_transaction().await?;
183
184        let find_one_result: bson::Document = coll
185            .find_one(bson::doc! { "x": 42 })
186            .await
187            .unwrap()
188            .unwrap();
189
190        assert_eq!(42, find_one_result.get_i32("x").unwrap());
191        Ok(())
192    }
193}