#[allow(unused_imports)]
use docker_wrapper::{
DockerCommand, ExecCommand, NetworkCreateCommand, NetworkRmCommand, RmCommand, RunCommand,
StopCommand,
};
#[allow(unused_imports)]
use std::time::{Duration, Instant};
#[allow(unused_imports)]
use tokio::net::TcpStream;
fn unique_name(prefix: &str) -> String {
format!("{}-{}", prefix, uuid::Uuid::new_v4())
}
async fn wait_for_port(host: &str, port: u16, timeout: Duration) -> Result<(), String> {
let start = Instant::now();
while start.elapsed() < timeout {
if TcpStream::connect(format!("{}:{}", host, port))
.await
.is_ok()
{
return Ok(());
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
Err(format!(
"Port {}:{} not ready after {:?}",
host, port, timeout
))
}
pub struct RedisFixture {
container_name: String,
port: u16,
password: Option<String>,
}
impl RedisFixture {
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
Self::with_config(6379, None).await
}
pub async fn with_config(
port: u16,
password: Option<String>,
) -> Result<Self, Box<dyn std::error::Error>> {
let container_name = unique_name("redis");
let mut cmd = RunCommand::new("redis:7-alpine")
.name(&container_name)
.port(port, 6379)
.detach()
.remove();
if let Some(ref pwd) = password {
cmd = cmd.cmd(vec![
"redis-server".to_string(),
"--requirepass".to_string(),
pwd.to_string(),
]);
}
cmd.execute().await?;
wait_for_port("localhost", port, Duration::from_secs(5)).await?;
Ok(Self {
container_name,
port,
password,
})
}
pub fn connection_string(&self) -> String {
match &self.password {
Some(pwd) => format!("redis://:{}@localhost:{}", pwd, self.port),
None => format!("redis://localhost:{}", self.port),
}
}
pub async fn exec(&self, args: Vec<&str>) -> Result<String, Box<dyn std::error::Error>> {
let mut cmd_args = vec!["redis-cli"];
if let Some(ref pwd) = self.password {
cmd_args.push("-a");
cmd_args.push(pwd);
}
cmd_args.extend(args);
let cmd_args_strings: Vec<String> = cmd_args.into_iter().map(|s| s.to_string()).collect();
let output = ExecCommand::new(&self.container_name, cmd_args_strings)
.execute()
.await?;
Ok(output.stdout)
}
pub async fn cleanup(self) -> Result<(), Box<dyn std::error::Error>> {
StopCommand::new(&self.container_name).execute().await?;
Ok(())
}
}
pub struct PostgresFixture {
container_name: String,
database: String,
username: String,
password: String,
port: u16,
}
impl PostgresFixture {
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
Self::with_config("testdb", "testuser", "testpass", 5432).await
}
pub async fn with_config(
database: &str,
username: &str,
password: &str,
port: u16,
) -> Result<Self, Box<dyn std::error::Error>> {
let container_name = unique_name("postgres");
let cid = RunCommand::new("postgres:15-alpine")
.name(&container_name)
.env("POSTGRES_DB", database)
.env("POSTGRES_USER", username)
.env("POSTGRES_PASSWORD", password)
.port_dyn(5432)
.detach()
.remove()
.execute()
.await?;
let mapped_ports = cid.port_mappings().await?;
let host_port = mapped_ports.first().map(|m| m.host_port).unwrap_or(port);
wait_for_port("localhost", host_port, Duration::from_secs(10)).await?;
tokio::time::sleep(Duration::from_secs(2)).await;
let mut ready = false;
for _ in 0..30 {
let probe = ExecCommand::new(
&container_name,
vec![
"psql".to_string(),
"-U".to_string(),
username.to_string(),
"-d".to_string(),
database.to_string(),
"-c".to_string(),
"SELECT 1".to_string(),
],
)
.env("PGPASSWORD", password)
.execute()
.await;
if let Ok(out) = probe {
if out.exit_code == 0 {
ready = true;
break;
}
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
if !ready {
return Err(format!(
"PostgreSQL in container {} did not become ready in time",
container_name
)
.into());
}
Ok(Self {
container_name,
database: database.to_string(),
username: username.to_string(),
password: password.to_string(),
port: host_port,
})
}
pub fn connection_string(&self) -> String {
format!(
"postgresql://{}:{}@localhost:{}/{}",
self.username, self.password, self.port, self.database
)
}
pub async fn exec_sql(&self, sql: &str) -> Result<String, Box<dyn std::error::Error>> {
let output = ExecCommand::new(
&self.container_name,
vec![
"psql".to_string(),
"-U".to_string(),
self.username.clone(),
"-d".to_string(),
self.database.clone(),
"-c".to_string(),
sql.to_string(),
],
)
.env("PGPASSWORD", &self.password)
.execute()
.await?;
Ok(output.stdout)
}
pub async fn cleanup(self) -> Result<(), Box<dyn std::error::Error>> {
StopCommand::new(&self.container_name).execute().await?;
Ok(())
}
}
pub struct MongoFixture {
container_name: String,
port: u16,
username: Option<String>,
password: Option<String>,
}
impl MongoFixture {
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
Self::with_auth(None, None, 27017).await
}
pub async fn with_auth(
username: Option<&str>,
password: Option<&str>,
port: u16,
) -> Result<Self, Box<dyn std::error::Error>> {
let container_name = unique_name("mongo");
let mut cmd = RunCommand::new("mongo:6")
.name(&container_name)
.port(port, 27017)
.detach()
.remove();
if let (Some(user), Some(pwd)) = (username, password) {
cmd = cmd
.env("MONGO_INITDB_ROOT_USERNAME", user)
.env("MONGO_INITDB_ROOT_PASSWORD", pwd);
}
cmd.execute().await?;
wait_for_port("localhost", port, Duration::from_secs(10)).await?;
Ok(Self {
container_name,
port,
username: username.map(String::from),
password: password.map(String::from),
})
}
pub fn connection_string(&self) -> String {
match (&self.username, &self.password) {
(Some(user), Some(pwd)) => {
format!("mongodb://{}:{}@localhost:{}", user, pwd, self.port)
}
_ => format!("mongodb://localhost:{}", self.port),
}
}
pub async fn cleanup(self) -> Result<(), Box<dyn std::error::Error>> {
StopCommand::new(&self.container_name).execute().await?;
Ok(())
}
}
pub struct TestEnvironment {
network_name: String,
containers: Vec<String>,
}
impl TestEnvironment {
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
let network_name = unique_name("test-net");
NetworkCreateCommand::new(&network_name).execute().await?;
Ok(Self {
network_name,
containers: Vec::new(),
})
}
pub async fn with_redis(&mut self) -> Result<String, Box<dyn std::error::Error>> {
let container_name = unique_name("env-redis");
RunCommand::new("redis:7-alpine")
.name(&container_name)
.network(&self.network_name)
.detach()
.remove()
.execute()
.await?;
self.containers.push(container_name.clone());
Ok(container_name)
}
pub async fn with_postgres(&mut self) -> Result<String, Box<dyn std::error::Error>> {
let container_name = unique_name("env-postgres");
RunCommand::new("postgres:15-alpine")
.name(&container_name)
.network(&self.network_name)
.env("POSTGRES_PASSWORD", "test")
.detach()
.remove()
.execute()
.await?;
self.containers.push(container_name.clone());
Ok(container_name)
}
pub fn network(&self) -> &str {
&self.network_name
}
pub async fn cleanup(self) -> Result<(), Box<dyn std::error::Error>> {
for container in &self.containers {
let _ = StopCommand::new(container).execute().await;
}
NetworkRmCommand::new(&self.network_name).execute().await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_with_redis_fixture() {
let redis = RedisFixture::new()
.await
.expect("Failed to create Redis fixture");
println!("Redis URL: {}", redis.connection_string());
let result = redis
.exec(vec!["PING"])
.await
.expect("Failed to ping Redis");
assert!(result.contains("PONG"));
redis
.exec(vec!["SET", "test_key", "test_value"])
.await
.expect("Failed to set value");
let value = redis
.exec(vec!["GET", "test_key"])
.await
.expect("Failed to get value");
assert!(value.contains("test_value"));
redis.cleanup().await.expect("Failed to cleanup Redis");
}
#[tokio::test]
async fn test_with_postgres_fixture() {
let postgres = PostgresFixture::new()
.await
.expect("Failed to create PostgreSQL fixture");
postgres
.exec_sql("CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(100))")
.await
.expect("Failed to create table");
postgres
.exec_sql("INSERT INTO users (name) VALUES ('Alice'), ('Bob')")
.await
.expect("Failed to insert data");
let result = postgres
.exec_sql("SELECT COUNT(*) FROM users")
.await
.expect("Failed to query data");
assert!(result.contains("2"));
postgres
.cleanup()
.await
.expect("Failed to cleanup PostgreSQL");
}
#[tokio::test]
async fn test_with_environment() {
let mut env = TestEnvironment::new()
.await
.expect("Failed to create test environment");
let redis_name = env.with_redis().await.expect("Failed to add Redis");
let postgres_name = env.with_postgres().await.expect("Failed to add PostgreSQL");
println!("Started Redis: {}", redis_name);
println!("Started PostgreSQL: {}", postgres_name);
println!("Network: {}", env.network());
env.cleanup().await.expect("Failed to cleanup environment");
}
}
fn main() {
println!("Test Fixtures Example");
println!("====================");
println!();
println!("This example demonstrates reusable test fixtures for:");
println!("- Redis");
println!("- PostgreSQL");
println!("- MongoDB");
println!("- Multi-service environments");
println!();
println!("This is a conceptual example showing test fixture patterns.");
println!("Implementation details may need adjustment for your specific use case.");
println!();
println!("See the source code for fixture patterns.");
}