testcontainers_modules/mysql/
mod.rs

1use std::borrow::Cow;
2
3use testcontainers::{core::WaitFor, CopyDataSource, CopyToContainer, Image};
4
5const NAME: &str = "mysql";
6const TAG: &str = "8.1";
7
8/// Module to work with [`MySQL`] inside of tests.
9///
10/// Starts an instance of MySQL with no password set for the root user and a default database named `test` created.
11///
12/// This module is based on the officlal [`MySQL docker image`].
13///
14/// # Example
15/// ```
16/// use testcontainers_modules::{mysql, testcontainers::runners::SyncRunner};
17///
18/// let mysql_instance = mysql::Mysql::default().start().unwrap();
19/// let mysql_url = format!(
20///     "mysql://{}:{}/test",
21///     mysql_instance.get_host().unwrap(),
22///     mysql_instance.get_host_port_ipv4(3306).unwrap()
23/// );
24/// ```
25///
26/// [`MySQL`]: https://www.mysql.com/
27/// [`MySQL docker image`]: https://hub.docker.com/_/mysql
28#[derive(Debug, Default, Clone)]
29pub struct Mysql {
30    copy_to_sources: Vec<CopyToContainer>,
31}
32impl Mysql {
33    /// Registers sql to be executed automatically when the container starts.
34    /// Can be called multiple times to add (not override) scripts.
35    ///
36    /// # Example
37    ///
38    /// ```
39    /// # use testcontainers_modules::mysql::Mysql;
40    /// let mysql_image = Mysql::default().with_init_sql(
41    ///     "CREATE TABLE foo (bar varchar(255));"
42    ///         .to_string()
43    ///         .into_bytes(),
44    /// );
45    /// ```
46    ///
47    /// ```rust,ignore
48    /// # use testcontainers_modules::mysql::Mysql;
49    /// let mysql_image = Mysql::default()
50    ///                                .with_init_sql(include_str!("path_to_init.sql").to_string().into_bytes());
51    /// ```
52    pub fn with_init_sql(mut self, init_sql: impl Into<CopyDataSource>) -> Self {
53        let target = format!(
54            "/docker-entrypoint-initdb.d/init_{i}.sql",
55            i = self.copy_to_sources.len()
56        );
57        self.copy_to_sources
58            .push(CopyToContainer::new(init_sql.into(), target));
59        self
60    }
61}
62
63impl Image for Mysql {
64    fn name(&self) -> &str {
65        NAME
66    }
67
68    fn tag(&self) -> &str {
69        TAG
70    }
71
72    fn ready_conditions(&self) -> Vec<WaitFor> {
73        vec![
74            WaitFor::message_on_stderr("X Plugin ready for connections. Bind-address"),
75            WaitFor::message_on_stderr("/usr/sbin/mysqld: ready for connections."),
76        ]
77    }
78
79    fn env_vars(
80        &self,
81    ) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
82        [
83            ("MYSQL_DATABASE", "test"),
84            ("MYSQL_ALLOW_EMPTY_PASSWORD", "yes"),
85        ]
86    }
87    fn copy_to_sources(&self) -> impl IntoIterator<Item = &CopyToContainer> {
88        &self.copy_to_sources
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use mysql::prelude::Queryable;
95    use testcontainers::core::IntoContainerPort;
96
97    use crate::{
98        mysql::Mysql as MysqlImage,
99        testcontainers::{runners::SyncRunner, ImageExt},
100    };
101
102    #[test]
103    fn mysql_with_init_sql() -> Result<(), Box<dyn std::error::Error + 'static>> {
104        let node = crate::mysql::Mysql::default()
105            .with_init_sql(
106                "CREATE TABLE foo (bar varchar(255));"
107                    .to_string()
108                    .into_bytes(),
109            )
110            .start()?;
111
112        let connection_string = &format!(
113            "mysql://root@{}:{}/test",
114            node.get_host()?,
115            node.get_host_port_ipv4(3306.tcp())?
116        );
117        let mut conn = mysql::Conn::new(mysql::Opts::from_url(connection_string).unwrap()).unwrap();
118
119        let rows: Vec<String> = conn.query("INSERT INTO foo(bar) VALUES ('blub')").unwrap();
120        assert_eq!(rows.len(), 0);
121
122        let rows: Vec<String> = conn.query("SELECT bar FROM foo").unwrap();
123        assert_eq!(rows.len(), 1);
124        Ok(())
125    }
126
127    #[test]
128    fn mysql_one_plus_one() -> Result<(), Box<dyn std::error::Error + 'static>> {
129        let mysql_image = MysqlImage::default();
130        let node = mysql_image.start()?;
131
132        let connection_string = &format!(
133            "mysql://root@{}:{}/mysql",
134            node.get_host()?,
135            node.get_host_port_ipv4(3306)?
136        );
137        let mut conn = mysql::Conn::new(mysql::Opts::from_url(connection_string).unwrap()).unwrap();
138
139        let first_row = conn.query_first("SELECT 1 + 1;").unwrap();
140        assert_eq!(first_row, Some(2));
141
142        let first_column: i32 = first_row.unwrap();
143        assert_eq!(first_column, 2);
144        Ok(())
145    }
146
147    #[test]
148    fn mysql_custom_version() -> Result<(), Box<dyn std::error::Error + 'static>> {
149        let image = MysqlImage::default().with_tag("8.0.34");
150        let node = image.start()?;
151
152        let connection_string = &format!(
153            "mysql://root@{}:{}/mysql",
154            node.get_host()?,
155            node.get_host_port_ipv4(3306)?
156        );
157
158        let mut conn = mysql::Conn::new(mysql::Opts::from_url(connection_string).unwrap()).unwrap();
159        let first_row = conn.query_first("SELECT version()").unwrap();
160        assert_eq!(first_row, Some(String::from("8.0.34")));
161        Ok(())
162    }
163}