testcontainers_modules/meilisearch/
mod.rs1use std::{borrow::Cow, collections::HashMap};
2
3use parse_display::{Display, FromStr};
4use testcontainers::{
5 core::{wait::HttpWaitStrategy, ContainerPort, WaitFor},
6 Image,
7};
8
9const NAME: &str = "getmeili/meilisearch";
10const TAG: &str = "v1.8.3";
11pub const MEILISEARCH_PORT: ContainerPort = ContainerPort::Tcp(7700);
16
17#[derive(Debug, Clone)]
39pub struct Meilisearch {
40 env_vars: HashMap<String, String>,
41}
42
43#[derive(Display, FromStr, Default, Debug, Clone, Copy, Eq, PartialEq)]
45#[display(style = "lowercase")]
46pub enum Environment {
47 Production,
51 #[default]
55 Development,
56}
57
58#[derive(Display, FromStr, Default, Debug, Clone, Copy, Eq, PartialEq)]
60#[display(style = "UPPERCASE")]
61pub enum LogLevel {
62 Error,
64 Warn,
66 #[default]
69 Info,
70 Debug,
72 Trace,
74 Off,
76}
77
78impl Meilisearch {
79 pub fn with_master_key(mut self, master_key: &str) -> Self {
84 self.env_vars
85 .insert("MEILI_MASTER_KEY".to_owned(), master_key.to_owned());
86 self
87 }
88
89 pub fn with_analytics(mut self, enabled: bool) -> Self {
95 if enabled {
96 self.env_vars.remove("MEILI_NO_ANALYTICS");
97 } else {
98 self.env_vars
99 .insert("MEILI_NO_ANALYTICS".to_owned(), "true".to_owned());
100 }
101 self
102 }
103
104 pub fn with_environment(mut self, environment: Environment) -> Self {
110 self.env_vars
111 .insert("MEILI_ENV".to_owned(), environment.to_string());
112 self
113 }
114
115 pub fn with_log_level(mut self, level: LogLevel) -> Self {
120 self.env_vars
121 .insert("MEILI_LOG_LEVEL".to_owned(), level.to_string());
122 self
123 }
124}
125
126impl Default for Meilisearch {
127 fn default() -> Self {
134 let mut env_vars = HashMap::new();
135 env_vars.insert("MEILI_NO_ANALYTICS".to_owned(), "true".to_owned());
136 Self { env_vars }
137 }
138}
139
140impl Image for Meilisearch {
141 fn name(&self) -> &str {
142 NAME
143 }
144
145 fn tag(&self) -> &str {
146 TAG
147 }
148
149 fn ready_conditions(&self) -> Vec<WaitFor> {
150 vec![WaitFor::http(
153 HttpWaitStrategy::new("/health")
154 .with_expected_status_code(200_u16)
155 .with_body(r#"{ "status": "available" }"#.as_bytes()),
156 )]
157 }
158
159 fn env_vars(
160 &self,
161 ) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
162 &self.env_vars
163 }
164
165 fn expose_ports(&self) -> &[ContainerPort] {
166 &[MEILISEARCH_PORT]
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use meilisearch_sdk::{client::Client, indexes::Index};
173 use serde::{Deserialize, Serialize};
174 use testcontainers::{runners::AsyncRunner, ImageExt};
175
176 use super::*;
177 #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
178 struct Movie {
179 id: i64,
180 title: String,
181 }
182
183 impl From<(i64, &str)> for Movie {
184 fn from((id, title): (i64, &str)) -> Self {
185 Self {
186 id,
187 title: title.to_owned(),
188 }
189 }
190 }
191
192 impl Movie {
193 fn examples() -> Vec<Self> {
194 vec![
195 Movie::from((1, "The Shawshank Redemption")),
196 Movie::from((2, "The Godfather")),
197 Movie::from((3, "The Dark Knight")),
198 Movie::from((4, "Pulp Fiction")),
199 Movie::from((5, "The Lord of the Rings: The Return of the King")),
200 Movie::from((6, "Forrest Gump")),
201 Movie::from((7, "Inception")),
202 Movie::from((8, "Fight Club")),
203 Movie::from((9, "The Matrix")),
204 Movie::from((10, "Goodfellas")),
205 ]
206 }
207 async fn get_index_with_loaded_examples(
208 client: &Client,
209 ) -> Result<Index, Box<dyn std::error::Error + 'static>> {
210 let task = client
211 .create_index("movies", None)
212 .await?
213 .wait_for_completion(client, None, None)
214 .await?;
215 let movies = task.try_make_index(client).unwrap();
216 assert_eq!(movies.as_ref(), "movies");
217 movies
218 .add_documents(&Movie::examples(), Some("id"))
219 .await?
220 .wait_for_completion(client, None, None)
221 .await?;
222 Ok(movies)
223 }
224 }
225
226 #[tokio::test]
227 async fn meilisearch_noauth() -> Result<(), Box<dyn std::error::Error + 'static>> {
228 let meilisearch_image = Meilisearch::default();
229 let node = meilisearch_image.start().await?;
230
231 let connection_string = &format!(
232 "http://{}:{}",
233 node.get_host().await?,
234 node.get_host_port_ipv4(7700).await?,
235 );
236 let auth: Option<String> = None; let client = Client::new(connection_string, auth).unwrap();
238
239 let res = client.health().await.unwrap();
241 assert_eq!(res.status, "available");
242
243 let movies = Movie::get_index_with_loaded_examples(&client).await?;
245 let res = movies
246 .search()
247 .with_query("Dark Knig")
248 .with_limit(5)
249 .execute::<Movie>()
250 .await?;
251 let results = res
252 .hits
253 .into_iter()
254 .map(|r| r.result)
255 .collect::<Vec<Movie>>();
256 assert_eq!(
257 results,
258 vec![Movie {
259 id: 3,
260 title: String::from("The Dark Knight")
261 }]
262 );
263 assert_eq!(res.estimated_total_hits, Some(1));
264
265 Ok(())
266 }
267
268 #[tokio::test]
269 async fn meilisearch_custom_version() -> Result<(), Box<dyn std::error::Error + 'static>> {
270 let master_key = "secret master key".to_owned();
271 let meilisearch_image = Meilisearch::default()
272 .with_master_key(&master_key)
273 .with_tag("v1.0");
274 let node = meilisearch_image.start().await?;
275
276 let connection_string = &format!(
277 "http://{}:{}",
278 node.get_host().await?,
279 node.get_host_port_ipv4(7700).await?,
280 );
281 let client = Client::new(connection_string, Some(master_key)).unwrap();
282
283 let movies = Movie::get_index_with_loaded_examples(&client).await?;
285 let res = movies
286 .search()
287 .with_query("Dark Knig")
288 .execute::<Movie>()
289 .await?;
290 let result_ids = res
291 .hits
292 .into_iter()
293 .map(|r| r.result.id)
294 .collect::<Vec<i64>>();
295 assert_eq!(result_ids, vec![3]);
296 Ok(())
297 }
298
299 #[tokio::test]
300 async fn meilisearch_without_logging_in_production_environment(
301 ) -> Result<(), Box<dyn std::error::Error + 'static>> {
302 let master_key = "secret master key".to_owned();
303 let meilisearch_image = Meilisearch::default()
304 .with_environment(Environment::Production)
305 .with_log_level(LogLevel::Off)
306 .with_master_key(&master_key);
307 let node = meilisearch_image.start().await?;
308
309 let connection_string = &format!(
310 "http://{}:{}",
311 node.get_host().await?,
312 node.get_host_port_ipv4(7700).await?,
313 );
314 let client = Client::new(connection_string, Some(master_key)).unwrap();
315
316 let movies = Movie::get_index_with_loaded_examples(&client).await?;
318 let res = movies
319 .search()
320 .with_query("Dark Knig")
321 .execute::<Movie>()
322 .await?;
323 let result_ids = res
324 .hits
325 .into_iter()
326 .map(|r| r.result.id)
327 .collect::<Vec<i64>>();
328 assert_eq!(result_ids, vec![3]);
329 Ok(())
330 }
331}