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 = "v2.2";
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 unauthenticated flag for the SurrealDB instance.
69    pub fn with_unauthenticated(mut self) -> Self {
70        self.env_vars
71            .insert("SURREAL_UNAUTHENTICATED".to_owned(), "true".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_stdout("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().with_unauthenticated().start().await?;
205        let host_port = node.get_host_port_ipv4(SURREALDB_PORT).await?;
206        let url = format!("127.0.0.1:{host_port}");
207
208        let db: Surreal<Client> = Surreal::init();
209        db.connect::<Ws>(url).await.unwrap();
210        db.use_ns("test").use_db("test").await.unwrap();
211
212        db.create::<Option<Person>>(("person", "tobie"))
213            .content(Person {
214                title: "Founder & CEO".to_string(),
215                name: Name {
216                    first: "Tobie".to_string(),
217                    last: "Morgan Hitchcock".to_string(),
218                },
219                marketing: true,
220            })
221            .await
222            .unwrap();
223
224        let result = db
225            .select::<Option<Person>>(("person", "tobie"))
226            .await
227            .unwrap();
228
229        assert!(result.is_some());
230        let result = result.unwrap();
231
232        assert_eq!(result.title, "Founder & CEO");
233        assert_eq!(result.name.first, "Tobie");
234        assert_eq!(result.name.last, "Morgan Hitchcock");
235        assert!(result.marketing);
236        Ok(())
237    }
238}