testcontainers_modules/surrealdb/
mod.rs

1use std::{borrow::Cow, collections::HashMap};
2
3use testcontainers::{
4    core::{ContainerPort, WaitFor},
5    Image,
6};
7
8const NAME: &str = "surrealdb/surrealdb";
9const TAG: &str = "v1.1.1";
10
11/// Port that the [`SurrealDB`] container has internally
12/// Can be rebound externally via [`testcontainers::core::ImageExt::with_mapped_port`]
13///
14/// [`SurrealDB`]: https://surrealdb.com/
15pub const SURREALDB_PORT: ContainerPort = ContainerPort::Tcp(8000);
16
17/// Module to work with [`SurrealDB`] inside of tests.
18/// Starts an instance of SurrealDB.
19/// This module is based on the official [`SurrealDB docker image`].
20/// Default user and password is `root`, and exposed port is `8000` ([`SURREALDB_PORT`]).
21/// # Example
22/// ```
23/// # use ::surrealdb::{
24/// #    engine::remote::ws::{Client, Ws},
25/// #    Surreal,
26/// # };
27/// use testcontainers_modules::{surrealdb, testcontainers::runners::SyncRunner};
28///
29/// let surrealdb_instance = surrealdb::SurrealDb::default().start().unwrap();
30///
31/// let connection_string = format!(
32///     "127.0.0.1:{}",
33///     surrealdb_instance
34///         .get_host_port_ipv4(surrealdb::SURREALDB_PORT)
35///         .unwrap(),
36/// );
37///
38/// # let runtime = tokio::runtime::Runtime::new().unwrap();
39/// # runtime.block_on(async {
40/// let db: Surreal<Client> = Surreal::init();
41/// db.connect::<Ws>(connection_string)
42///     .await
43///     .expect("Failed to connect to SurrealDB");
44/// # });
45/// ```
46/// [`SurrealDB`]: https://surrealdb.com/
47/// [`SurrealDB docker image`]: https://hub.docker.com/r/surrealdb/surrealdb
48#[derive(Debug, Clone)]
49pub struct SurrealDb {
50    env_vars: HashMap<String, String>,
51}
52
53impl SurrealDb {
54    /// Sets the user for the SurrealDB instance.
55    pub fn with_user(mut self, user: &str) -> Self {
56        self.env_vars
57            .insert("SURREAL_USER".to_owned(), user.to_owned());
58        self
59    }
60
61    /// Sets the password for the SurrealDB instance.
62    pub fn with_password(mut self, password: &str) -> Self {
63        self.env_vars
64            .insert("SURREAL_PASS".to_owned(), password.to_owned());
65        self
66    }
67
68    /// Sets authentication for the SurrealDB instance.
69    pub fn with_authentication(mut self, authentication: bool) -> Self {
70        self.env_vars
71            .insert("SURREAL_AUTH".to_owned(), authentication.to_string());
72        self
73    }
74
75    /// Sets strict mode for the SurrealDB instance.
76    pub fn with_strict(mut self, strict: bool) -> Self {
77        self.env_vars
78            .insert("SURREAL_STRICT".to_owned(), strict.to_string());
79        self
80    }
81
82    /// Sets all capabilities for the SurrealDB instance.
83    pub fn with_all_capabilities(mut self, allow_all: bool) -> Self {
84        self.env_vars
85            .insert("SURREAL_CAPS_ALLOW_ALL".to_owned(), allow_all.to_string());
86        self
87    }
88}
89
90impl Default for SurrealDb {
91    fn default() -> Self {
92        let mut env_vars = HashMap::new();
93        env_vars.insert("SURREAL_USER".to_owned(), "root".to_owned());
94        env_vars.insert("SURREAL_PASS".to_owned(), "root".to_owned());
95        env_vars.insert("SURREAL_AUTH".to_owned(), "true".to_owned());
96        env_vars.insert("SURREAL_CAPS_ALLOW_ALL".to_owned(), "true".to_owned());
97        env_vars.insert("SURREAL_PATH".to_owned(), "memory".to_owned());
98
99        Self { env_vars }
100    }
101}
102
103impl Image for SurrealDb {
104    fn name(&self) -> &str {
105        NAME
106    }
107
108    fn tag(&self) -> &str {
109        TAG
110    }
111
112    fn ready_conditions(&self) -> Vec<WaitFor> {
113        vec![WaitFor::message_on_stderr("Started web server on ")]
114    }
115
116    fn env_vars(
117        &self,
118    ) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
119        &self.env_vars
120    }
121
122    fn cmd(&self) -> impl IntoIterator<Item = impl Into<Cow<'_, str>>> {
123        ["start"]
124    }
125
126    fn expose_ports(&self) -> &[ContainerPort] {
127        &[SURREALDB_PORT]
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use serde::{Deserialize, Serialize};
134    use surrealdb::{
135        engine::remote::ws::{Client, Ws},
136        opt::auth::Root,
137        Surreal,
138    };
139    use testcontainers::runners::AsyncRunner;
140
141    use super::*;
142
143    #[derive(Debug, Serialize, Deserialize)]
144    struct Name {
145        first: String,
146        last: String,
147    }
148
149    #[derive(Debug, Serialize, Deserialize)]
150    struct Person {
151        title: String,
152        name: Name,
153        marketing: bool,
154    }
155
156    #[tokio::test]
157    async fn surrealdb_select() -> Result<(), Box<dyn std::error::Error + 'static>> {
158        let _ = pretty_env_logger::try_init();
159        let node = SurrealDb::default().start().await?;
160        let host_port = node.get_host_port_ipv4(SURREALDB_PORT).await?;
161        let url = format!("127.0.0.1:{host_port}");
162
163        let db: Surreal<Client> = Surreal::init();
164        db.connect::<Ws>(url).await.unwrap();
165        db.signin(Root {
166            username: "root",
167            password: "root",
168        })
169        .await
170        .unwrap();
171
172        db.use_ns("test").use_db("test").await.unwrap();
173
174        db.create::<Option<Person>>(("person", "tobie"))
175            .content(Person {
176                title: "Founder & CEO".to_string(),
177                name: Name {
178                    first: "Tobie".to_string(),
179                    last: "Morgan Hitchcock".to_string(),
180                },
181                marketing: true,
182            })
183            .await
184            .unwrap();
185
186        let result = db
187            .select::<Option<Person>>(("person", "tobie"))
188            .await
189            .unwrap();
190
191        assert!(result.is_some());
192        let result = result.unwrap();
193
194        assert_eq!(result.title, "Founder & CEO");
195        assert_eq!(result.name.first, "Tobie");
196        assert_eq!(result.name.last, "Morgan Hitchcock");
197        assert!(result.marketing);
198        Ok(())
199    }
200
201    #[tokio::test]
202    async fn surrealdb_no_auth() -> Result<(), Box<dyn std::error::Error + 'static>> {
203        let _ = pretty_env_logger::try_init();
204        let node = SurrealDb::default()
205            .with_authentication(false)
206            .start()
207            .await?;
208        let host_port = node.get_host_port_ipv4(SURREALDB_PORT).await?;
209        let url = format!("127.0.0.1:{host_port}");
210
211        let db: Surreal<Client> = Surreal::init();
212        db.connect::<Ws>(url).await.unwrap();
213        db.use_ns("test").use_db("test").await.unwrap();
214
215        db.create::<Option<Person>>(("person", "tobie"))
216            .content(Person {
217                title: "Founder & CEO".to_string(),
218                name: Name {
219                    first: "Tobie".to_string(),
220                    last: "Morgan Hitchcock".to_string(),
221                },
222                marketing: true,
223            })
224            .await
225            .unwrap();
226
227        let result = db
228            .select::<Option<Person>>(("person", "tobie"))
229            .await
230            .unwrap();
231
232        assert!(result.is_some());
233        let result = result.unwrap();
234
235        assert_eq!(result.title, "Founder & CEO");
236        assert_eq!(result.name.first, "Tobie");
237        assert_eq!(result.name.last, "Morgan Hitchcock");
238        assert!(result.marketing);
239        Ok(())
240    }
241}