testcontainers_modules/mssql_server/
mod.rs

1use std::{borrow::Cow, collections::HashMap};
2
3use testcontainers::{core::WaitFor, Image};
4
5/// [Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server) module
6/// for [testcontainers](https://crates.io/crates/testcontainers).
7///
8/// This module is based on the
9/// [official Microsoft SQL Server for Linux Docker image](https://hub.docker.com/_/microsoft-mssql-server).
10/// Only amd64 images are available for SQL Server. If you use Apple silicon machines,
11/// you need to configure Rosetta emulation.
12///
13/// * [Change Docker Desktop settings on Mac | Docker Docs](https://docs.docker.com/desktop/settings/mac/#general)
14///
15/// # Example
16///
17/// ```
18/// use testcontainers_modules::{testcontainers::runners::SyncRunner, mssql_server};
19///
20/// let mssql_server = mssql_server::MssqlServer::default().with_accept_eula().start().unwrap();
21/// let ado_connection_string = format!(
22///    "Server=tcp:{},{};Database=test;User Id=sa;Password=yourStrong(!)Password;TrustServerCertificate=True;",
23///    mssql_server.get_host().unwrap(),
24///    mssql_server.get_host_port_ipv4(1433).unwrap()
25/// );
26/// ```
27///
28/// # Environment variables
29///
30/// Refer to the [documentation](https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-configure-environment-variables)
31/// for a complite list of environment variables.
32///
33/// Following environment variables are required.
34/// A image provided by this module has default values for them.
35///
36/// ## EULA Acceptance
37///
38/// Due to licensing restrictions you are required to explicitly accept an End User License Agreement (EULA) for the MS SQL Server container image.
39/// This is facilitated through the explicit call of `with_accept_eula` function.
40///
41/// Please see the [microsoft-mssql-server image documentation](https://hub.docker.com/_/microsoft-mssql-server#environment-variables) for a link to the EULA document.
42///
43/// ## `MSSQL_SA_PASSWORD`
44///
45/// The SA user password. This password is required to conform to the
46/// [strong password policy](https://learn.microsoft.com/en-us/sql/relational-databases/security/password-policy?view=sql-server-ver15#password-complexity).
47/// The default value is `yourStrong(!)Password`.
48///
49/// ## `MSSQL_PID`
50///
51/// The edition of SQL Server.
52/// The default value is `Developer`, which will run the container using the Developer Edition.
53#[derive(Debug, Clone)]
54pub struct MssqlServer {
55    env_vars: HashMap<String, String>,
56}
57
58impl MssqlServer {
59    const NAME: &'static str = "mcr.microsoft.com/mssql/server";
60    const TAG: &'static str = "2022-CU14-ubuntu-22.04";
61    /// Default Password for `MSSQL_SA_PASSWORD`.
62    /// If you want to set your own password, please use [`with_sa_password`]
63    pub const DEFAULT_SA_PASSWORD: &'static str = "yourStrong(!)Password";
64
65    /// Sets the password as `MSSQL_SA_PASSWORD`.
66    pub fn with_sa_password(mut self, password: impl Into<String>) -> Self {
67        self.env_vars
68            .insert("MSSQL_SA_PASSWORD".into(), password.into());
69        self
70    }
71
72    /// Due to licensing restrictions you are required to explicitly accept an End User License Agreement (EULA) for the MS SQL Server container image.
73    /// This is facilitated through the `with_accept_eula` function.
74    ///
75    /// Please see the [microsoft-mssql-server image documentation](https://hub.docker.com/_/microsoft-mssql-server#environment-variables) for a link to the EULA document.
76    pub fn with_accept_eula(mut self) -> Self {
77        self.env_vars.insert("ACCEPT_EULA".into(), "Y".into());
78        self
79    }
80}
81
82impl Default for MssqlServer {
83    fn default() -> Self {
84        let mut env_vars = HashMap::new();
85        env_vars.insert(
86            "MSSQL_SA_PASSWORD".to_owned(),
87            Self::DEFAULT_SA_PASSWORD.to_owned(),
88        );
89        env_vars.insert("MSSQL_PID".to_owned(), "Developer".to_owned());
90
91        Self { env_vars }
92    }
93}
94
95impl Image for MssqlServer {
96    fn name(&self) -> &str {
97        Self::NAME
98    }
99
100    fn tag(&self) -> &str {
101        Self::TAG
102    }
103
104    fn ready_conditions(&self) -> Vec<WaitFor> {
105        // Wait until all system databases are recovered
106        vec![
107            WaitFor::message_on_stdout("SQL Server is now ready for client connections"),
108            WaitFor::message_on_stdout("Recovery is complete"),
109        ]
110    }
111
112    fn env_vars(
113        &self,
114    ) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
115        &self.env_vars
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use std::error;
122
123    use testcontainers::runners::AsyncRunner;
124    use tiberius::{AuthMethod, Client, Config};
125    use tokio::net::TcpStream;
126    use tokio_util::compat::{Compat, TokioAsyncWriteCompatExt};
127
128    use super::*;
129
130    #[tokio::test]
131    async fn one_plus_one() -> Result<(), Box<dyn error::Error>> {
132        let container = MssqlServer::default().with_accept_eula().start().await?;
133        let config = new_config(
134            container.get_host().await?,
135            container.get_host_port_ipv4(1433).await?,
136            MssqlServer::DEFAULT_SA_PASSWORD,
137        );
138        let mut client = get_mssql_client(config).await?;
139
140        let stream = client.query("SELECT 1 + 1", &[]).await?;
141        let row = stream.into_row().await?.unwrap();
142
143        assert_eq!(row.get::<i32, _>(0).unwrap(), 2);
144
145        Ok(())
146    }
147
148    #[tokio::test]
149    async fn custom_sa_password() -> Result<(), Box<dyn error::Error>> {
150        let image = MssqlServer::default()
151            .with_accept_eula()
152            .with_sa_password("yourStrongPassword123!");
153        let container = image.start().await?;
154        let config = new_config(
155            container.get_host().await?,
156            container.get_host_port_ipv4(1433).await?,
157            "yourStrongPassword123!",
158        );
159        let mut client = get_mssql_client(config).await?;
160
161        let stream = client.query("SELECT 1 + 1", &[]).await?;
162        let row = stream.into_row().await?.unwrap();
163
164        assert_eq!(row.get::<i32, _>(0).unwrap(), 2);
165
166        Ok(())
167    }
168
169    async fn get_mssql_client(
170        config: Config,
171    ) -> Result<Client<Compat<TcpStream>>, Box<dyn error::Error>> {
172        let tcp = TcpStream::connect(config.get_addr()).await?;
173        tcp.set_nodelay(true)?;
174
175        let client = Client::connect(config, tcp.compat_write()).await?;
176
177        Ok(client)
178    }
179
180    fn new_config(host: impl ToString, port: u16, password: &str) -> Config {
181        let mut config = Config::new();
182        config.host(host);
183        config.port(port);
184        config.authentication(AuthMethod::sql_server("sa", password));
185        config.trust_cert();
186
187        config
188    }
189}