|
| 1 | +use std::{borrow::Cow, collections::BTreeMap}; |
| 2 | + |
| 3 | +use testcontainers::{ |
| 4 | + core::{ContainerPort, WaitFor}, |
| 5 | + Image, |
| 6 | +}; |
| 7 | + |
| 8 | +const NAME: &str = "mcr.microsoft.com/azure-storage/azurite"; |
| 9 | +const TAG: &str = "3.34.0"; |
| 10 | + |
| 11 | +/// Port that [`Azurite`] uses internally for blob storage. |
| 12 | +pub const BLOB_PORT: ContainerPort = ContainerPort::Tcp(10000); |
| 13 | + |
| 14 | +/// Port that [`Azurite`] uses internally for queue. |
| 15 | +pub const QUEUE_PORT: ContainerPort = ContainerPort::Tcp(10001); |
| 16 | + |
| 17 | +/// Port that [`Azurite`] uses internally for table. |
| 18 | +const TABLE_PORT: ContainerPort = ContainerPort::Tcp(10002); |
| 19 | + |
| 20 | +const 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, Clone)] |
| 43 | +pub struct Azurite { |
| 44 | + tag: String, |
| 45 | + env_vars: BTreeMap<String, String>, |
| 46 | + loose: bool, |
| 47 | + skip_api_version_check: bool, |
| 48 | + disable_telemetry: bool, |
| 49 | +} |
| 50 | + |
| 51 | +impl Default for Azurite { |
| 52 | + fn default() -> Self { |
| 53 | + Self { |
| 54 | + tag: TAG.to_string(), |
| 55 | + env_vars: BTreeMap::new(), |
| 56 | + loose: false, |
| 57 | + skip_api_version_check: false, |
| 58 | + disable_telemetry: false, |
| 59 | + } |
| 60 | + } |
| 61 | +} |
| 62 | +impl Azurite { |
| 63 | + /// Create a new azurite instance with the latest image |
| 64 | + pub fn latest() -> Self { |
| 65 | + Self { |
| 66 | + tag: "latest".to_string(), |
| 67 | + ..Self::default() |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + /// Overrides the image tag. |
| 72 | + /// Check https://mcr.microsoft.com/v2/azure-storage/azurite/tags/list to see available tags. |
| 73 | + pub fn with_tag(self, tag: String) -> Self { |
| 74 | + Self { tag, ..self } |
| 75 | + } |
| 76 | + |
| 77 | + /// 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. |
| 78 | + /// |
| 79 | + /// - Uses `AZURITE_ACCOUNTS` key is used to store the accounts in the environment variables. |
| 80 | + /// - The format should be: `account1:key1[:key2];account2:key1[:key2];...` |
| 81 | + pub fn with_accounts(self, accounts: String) -> Self { |
| 82 | + let mut env_vars = self.env_vars; |
| 83 | + env_vars.insert(AZURITE_ACCOUNTS.to_owned(), accounts); |
| 84 | + Self { env_vars, ..self } |
| 85 | + } |
| 86 | + |
| 87 | + /// Disables strict mode |
| 88 | + pub fn with_loose(self) -> Self { |
| 89 | + Self { |
| 90 | + loose: true, |
| 91 | + ..self |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + /// Skips API version validation |
| 96 | + pub fn with_skip_api_version_check(self) -> Self { |
| 97 | + Self { |
| 98 | + skip_api_version_check: true, |
| 99 | + ..self |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + /// Disables telemetry data collection |
| 104 | + pub fn with_disable_telemetry(self) -> Self { |
| 105 | + Self { |
| 106 | + disable_telemetry: true, |
| 107 | + ..self |
| 108 | + } |
| 109 | + } |
| 110 | +} |
| 111 | +impl Image for Azurite { |
| 112 | + fn name(&self) -> &str { |
| 113 | + NAME |
| 114 | + } |
| 115 | + |
| 116 | + fn tag(&self) -> &str { |
| 117 | + &self.tag |
| 118 | + } |
| 119 | + |
| 120 | + fn ready_conditions(&self) -> Vec<WaitFor> { |
| 121 | + vec![WaitFor::message_on_stdout( |
| 122 | + "Azurite Table service is successfully listening at http://0.0.0.0:10002", |
| 123 | + )] |
| 124 | + } |
| 125 | + |
| 126 | + fn env_vars( |
| 127 | + &self, |
| 128 | + ) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> { |
| 129 | + &self.env_vars |
| 130 | + } |
| 131 | + |
| 132 | + fn cmd(&self) -> impl IntoIterator<Item = impl Into<Cow<'_, str>>> { |
| 133 | + let mut cmd = vec![ |
| 134 | + String::from("azurite"), |
| 135 | + String::from("--blobHost"), |
| 136 | + String::from("0.0.0.0"), |
| 137 | + String::from("--queueHost"), |
| 138 | + String::from("0.0.0.0"), |
| 139 | + String::from("--tableHost"), |
| 140 | + String::from("0.0.0.0"), |
| 141 | + ]; |
| 142 | + if self.loose { |
| 143 | + cmd.push(String::from("--loose")); |
| 144 | + } |
| 145 | + if self.skip_api_version_check { |
| 146 | + cmd.push(String::from("--skipApiVersionCheck")); |
| 147 | + } |
| 148 | + if self.disable_telemetry { |
| 149 | + cmd.push(String::from("--disableTelemetry")); |
| 150 | + } |
| 151 | + cmd |
| 152 | + } |
| 153 | + |
| 154 | + fn expose_ports(&self) -> &[ContainerPort] { |
| 155 | + &[BLOB_PORT, QUEUE_PORT, TABLE_PORT] |
| 156 | + } |
| 157 | +} |
| 158 | + |
| 159 | +#[cfg(test)] |
| 160 | +mod tests { |
| 161 | + use azure_storage::{prelude::*, CloudLocation}; |
| 162 | + use azure_storage_blobs::prelude::*; |
| 163 | + use base64::{prelude::BASE64_STANDARD, Engine}; |
| 164 | + |
| 165 | + use crate::azurite::{Azurite, BLOB_PORT}; |
| 166 | + |
| 167 | + #[tokio::test] |
| 168 | + async fn starts_with_async_runner() -> Result<(), Box<dyn std::error::Error + 'static>> { |
| 169 | + use testcontainers::runners::AsyncRunner; |
| 170 | + let azurite = Azurite::default(); |
| 171 | + azurite.start().await?; |
| 172 | + Ok(()) |
| 173 | + } |
| 174 | + |
| 175 | + #[test] |
| 176 | + fn starts_with_sync_runner() -> Result<(), Box<dyn std::error::Error + 'static>> { |
| 177 | + use testcontainers::runners::SyncRunner; |
| 178 | + let azurite = Azurite::default(); |
| 179 | + azurite.start()?; |
| 180 | + Ok(()) |
| 181 | + } |
| 182 | + |
| 183 | + #[test] |
| 184 | + fn starts_with_latest_tag() -> Result<(), Box<dyn std::error::Error + 'static>> { |
| 185 | + use testcontainers::runners::SyncRunner; |
| 186 | + let azurite = Azurite::latest().with_tag("latest".to_string()); |
| 187 | + azurite.start()?; |
| 188 | + Ok(()) |
| 189 | + } |
| 190 | + |
| 191 | + #[test] |
| 192 | + fn starts_with_tag() -> Result<(), Box<dyn std::error::Error + 'static>> { |
| 193 | + use testcontainers::runners::SyncRunner; |
| 194 | + let azurite = Azurite::default().with_tag("latest".to_string()); |
| 195 | + azurite.start()?; |
| 196 | + Ok(()) |
| 197 | + } |
| 198 | + |
| 199 | + #[test] |
| 200 | + fn starts_with_loose() -> Result<(), Box<dyn std::error::Error + 'static>> { |
| 201 | + use testcontainers::runners::SyncRunner; |
| 202 | + let azurite = Azurite::default().with_loose(); |
| 203 | + azurite.start()?; |
| 204 | + Ok(()) |
| 205 | + } |
| 206 | + |
| 207 | + #[test] |
| 208 | + fn starts_with_with_skip_api_version_check() -> Result<(), Box<dyn std::error::Error + 'static>> |
| 209 | + { |
| 210 | + use testcontainers::runners::SyncRunner; |
| 211 | + let azurite = Azurite::default().with_skip_api_version_check(); |
| 212 | + azurite.start()?; |
| 213 | + Ok(()) |
| 214 | + } |
| 215 | + |
| 216 | + #[tokio::test] |
| 217 | + async fn starts_with_accounts() -> Result<(), Box<dyn std::error::Error + 'static>> { |
| 218 | + use azure_core::auth::Secret; |
| 219 | + use testcontainers::runners::AsyncRunner; |
| 220 | + |
| 221 | + let data = b"key1"; |
| 222 | + let account_key = BASE64_STANDARD.encode(data); |
| 223 | + |
| 224 | + let account_name = "account1"; |
| 225 | + let container = Azurite::default() |
| 226 | + .with_accounts(format!("{}:{};", account_name, account_key)) |
| 227 | + .start() |
| 228 | + .await?; |
| 229 | + |
| 230 | + ClientBuilder::with_location( |
| 231 | + CloudLocation::Custom { |
| 232 | + account: account_name.to_string(), |
| 233 | + uri: format!( |
| 234 | + "http://127.0.0.1:{}/{}", |
| 235 | + container.get_host_port_ipv4(BLOB_PORT).await?, |
| 236 | + account_name |
| 237 | + ), |
| 238 | + }, |
| 239 | + StorageCredentials::access_key(account_name, Secret::new(account_key)), |
| 240 | + ) |
| 241 | + .container_client("container-name") |
| 242 | + .create() |
| 243 | + .await?; |
| 244 | + |
| 245 | + Ok(()) |
| 246 | + } |
| 247 | +} |
0 commit comments