testcontainers_modules/azurite/
mod.rs

1use std::{borrow::Cow, collections::BTreeMap};
2
3use testcontainers::{
4    core::{ContainerPort, WaitFor},
5    Image,
6};
7
8const NAME: &str = "mcr.microsoft.com/azure-storage/azurite";
9const TAG: &str = "3.34.0";
10
11/// Port that [`Azurite`] uses internally for blob storage.
12pub const BLOB_PORT: ContainerPort = ContainerPort::Tcp(10000);
13
14/// Port that [`Azurite`] uses internally for queue.
15pub const QUEUE_PORT: ContainerPort = ContainerPort::Tcp(10001);
16
17/// Port that [`Azurite`] uses internally for table.
18const TABLE_PORT: ContainerPort = ContainerPort::Tcp(10002);
19
20const AZURITE_ACCOUNTS: &str = "AZURITE_ACCOUNTS";
21
22/// Module to work with [`Azurite`] inside tests.
23///
24/// This module is based on the official [`Azurite docker image`].
25///
26/// # Example
27/// ```
28/// use testcontainers_modules::{
29///     azurite,
30///     azurite::{Azurite, BLOB_PORT},
31///     testcontainers::runners::SyncRunner,
32/// };
33///
34/// let azurite = Azurite::default().start().unwrap();
35/// let blob_port = azurite.get_host_port_ipv4(BLOB_PORT).unwrap();
36///
37/// // do something with the started azurite instance..
38/// ```
39///
40/// [`Azurite`]: https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?toc=%2Fazure%2Fstorage%2Fblobs%2Ftoc.json&bc=%2Fazure%2Fstorage%2Fblobs%2Fbreadcrumb%2Ftoc.json&tabs=visual-studio%2Cblob-storage
41/// [`Azurite docker image`]: https://hub.docker.com/r/microsoft/azure-storage-azurite
42#[derive(Debug, Default, Clone)]
43pub struct Azurite {
44    env_vars: BTreeMap<String, String>,
45    loose: bool,
46    skip_api_version_check: bool,
47    disable_telemetry: bool,
48}
49
50impl Azurite {
51    /// Sets the [Azurite accounts](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?toc=%2Fazure%2Fstorage%2Fblobs%2Ftoc.json&bc=%2Fazure%2Fstorage%2Fblobs%2Fbreadcrumb%2Ftoc.json&tabs=visual-studio%2Ctable-storage#custom-storage-accounts-and-keys) to be used by the instance.
52    ///
53    /// - Uses `AZURITE_ACCOUNTS` key is used to store the accounts in the environment variables.
54    /// - The format should be: `account1:key1[:key2];account2:key1[:key2];...`
55    pub fn with_accounts(self, accounts: String) -> Self {
56        let mut env_vars = self.env_vars;
57        env_vars.insert(AZURITE_ACCOUNTS.to_owned(), accounts);
58        Self { env_vars, ..self }
59    }
60
61    /// Disables strict mode
62    pub fn with_loose(self) -> Self {
63        Self {
64            loose: true,
65            ..self
66        }
67    }
68
69    /// Skips API version validation
70    pub fn with_skip_api_version_check(self) -> Self {
71        Self {
72            skip_api_version_check: true,
73            ..self
74        }
75    }
76
77    /// Disables telemetry data collection
78    pub fn with_disable_telemetry(self) -> Self {
79        Self {
80            disable_telemetry: true,
81            ..self
82        }
83    }
84}
85impl Image for Azurite {
86    fn name(&self) -> &str {
87        NAME
88    }
89
90    fn tag(&self) -> &str {
91        TAG
92    }
93
94    fn ready_conditions(&self) -> Vec<WaitFor> {
95        vec![WaitFor::message_on_stdout(
96            "Azurite Table service is successfully listening at http://0.0.0.0:10002",
97        )]
98    }
99
100    fn env_vars(
101        &self,
102    ) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
103        &self.env_vars
104    }
105
106    fn cmd(&self) -> impl IntoIterator<Item = impl Into<Cow<'_, str>>> {
107        let mut cmd = vec![
108            String::from("azurite"),
109            String::from("--blobHost"),
110            String::from("0.0.0.0"),
111            String::from("--queueHost"),
112            String::from("0.0.0.0"),
113            String::from("--tableHost"),
114            String::from("0.0.0.0"),
115        ];
116        if self.loose {
117            cmd.push(String::from("--loose"));
118        }
119        if self.skip_api_version_check {
120            cmd.push(String::from("--skipApiVersionCheck"));
121        }
122        if self.disable_telemetry {
123            cmd.push(String::from("--disableTelemetry"));
124        }
125        cmd
126    }
127
128    fn expose_ports(&self) -> &[ContainerPort] {
129        &[BLOB_PORT, QUEUE_PORT, TABLE_PORT]
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use azure_storage::{prelude::*, CloudLocation};
136    use azure_storage_blobs::prelude::*;
137    use base64::{prelude::BASE64_STANDARD, Engine};
138
139    use crate::azurite::{Azurite, BLOB_PORT};
140
141    #[tokio::test]
142    async fn starts_with_async_runner() -> Result<(), Box<dyn std::error::Error + 'static>> {
143        use testcontainers::runners::AsyncRunner;
144        let azurite = Azurite::default();
145        azurite.start().await?;
146        Ok(())
147    }
148
149    #[test]
150    fn starts_with_sync_runner() -> Result<(), Box<dyn std::error::Error + 'static>> {
151        use testcontainers::runners::SyncRunner;
152        let azurite = Azurite::default();
153        azurite.start()?;
154        Ok(())
155    }
156
157    #[test]
158    fn starts_with_loose() -> Result<(), Box<dyn std::error::Error + 'static>> {
159        use testcontainers::runners::SyncRunner;
160        let azurite = Azurite::default().with_loose();
161        azurite.start()?;
162        Ok(())
163    }
164
165    #[test]
166    fn starts_with_with_skip_api_version_check() -> Result<(), Box<dyn std::error::Error + 'static>>
167    {
168        use testcontainers::runners::SyncRunner;
169        let azurite = Azurite::default().with_skip_api_version_check();
170        azurite.start()?;
171        Ok(())
172    }
173
174    #[tokio::test]
175    async fn starts_with_accounts() -> Result<(), Box<dyn std::error::Error + 'static>> {
176        use azure_core::auth::Secret;
177        use testcontainers::runners::AsyncRunner;
178
179        let data = b"key1";
180        let account_key = BASE64_STANDARD.encode(data);
181
182        let account_name = "account1";
183        let container = Azurite::default()
184            .with_accounts(format!("{}:{};", account_name, account_key))
185            .start()
186            .await?;
187
188        ClientBuilder::with_location(
189            CloudLocation::Custom {
190                account: account_name.to_string(),
191                uri: format!(
192                    "http://127.0.0.1:{}/{}",
193                    container.get_host_port_ipv4(BLOB_PORT).await?,
194                    account_name
195                ),
196            },
197            StorageCredentials::access_key(account_name, Secret::new(account_key)),
198        )
199        .container_client("container-name")
200        .create()
201        .await?;
202
203        Ok(())
204    }
205}