#[derive(Debug, Clone)]
pub struct TestContainerConfig {
pub image: String,
pub name: String,
pub ports: Vec<u16>,
pub env_vars: std::collections::HashMap<String, String>,
pub remove_on_drop: bool,
}
impl Default for TestContainerConfig {
fn default() -> Self {
Self {
image: "postgres:15".to_string(),
name: "test-container".to_string(),
ports: vec![],
env_vars: std::collections::HashMap::new(),
remove_on_drop: true,
}
}
}
#[derive(Debug)]
pub struct TestContainer {
config: TestContainerConfig,
is_running: bool,
}
impl TestContainer {
pub fn new(config: TestContainerConfig) -> Self {
Self {
config,
is_running: false,
}
}
pub fn config(&self) -> &TestContainerConfig {
&self.config
}
pub fn is_running(&self) -> bool {
self.is_running
}
pub async fn start(&mut self) -> Result<(), String> {
log::info!("Starting test container: {}", self.config.name);
self.is_running = true;
Ok(())
}
pub async fn stop(&mut self) -> Result<(), String> {
log::info!("Stopping test container: {}", self.config.name);
self.is_running = false;
Ok(())
}
pub fn host(&self) -> String {
"localhost".to_string()
}
pub fn port(&self, service_port: u16) -> Option<u16> {
if self.config.ports.contains(&service_port) {
Some(service_port)
} else {
None
}
}
pub fn connection_url(&self, service: &str, port: u16) -> Option<String> {
if self.config.ports.contains(&port) {
Some(format!("{}://{}:{}", service, self.host(), port))
} else {
None
}
}
}
impl Drop for TestContainer {
fn drop(&mut self) {
if self.config.remove_on_drop && self.is_running {
log::info!("Dropping test container: {}", self.config.name);
}
}
}
#[derive(Debug)]
pub struct PostgreSqlContainer {
container: TestContainer,
database: String,
username: String,
password: String,
}
impl PostgreSqlContainer {
pub fn new() -> Self {
let mut config = TestContainerConfig {
image: "postgres:15".to_string(),
name: "test-postgres".to_string(),
ports: vec![5432],
env_vars: std::collections::HashMap::new(),
remove_on_drop: true,
};
config.env_vars.insert("POSTGRES_DB".to_string(), "testdb".to_string());
config.env_vars.insert("POSTGRES_USER".to_string(), "testuser".to_string());
config.env_vars.insert("POSTGRES_PASSWORD".to_string(), "testpass".to_string());
Self {
container: TestContainer::new(config),
database: "testdb".to_string(),
username: "testuser".to_string(),
password: "testpass".to_string(),
}
}
pub async fn start(&mut self) -> Result<(), String> {
self.container.start().await
}
pub async fn stop(&mut self) -> Result<(), String> {
self.container.stop().await
}
pub fn connection_url(&self) -> String {
if let Some(port) = self.container.port(5432) {
format!("postgres://{}:{}@{}:{}/{}",
self.username,
self.password,
self.container.host(),
port,
self.database
)
} else {
format!("postgres://{}:{}@localhost:{}/{}",
self.username,
self.password,
5432,
self.database
)
}
}
}
#[derive(Debug)]
pub struct NatsContainer {
container: TestContainer,
}
impl NatsContainer {
pub fn new() -> Self {
let config = TestContainerConfig {
image: "nats:2.10".to_string(),
name: "test-nats".to_string(),
ports: vec![4222],
env_vars: std::collections::HashMap::new(),
remove_on_drop: true,
};
let mut env_vars = std::collections::HashMap::new();
env_vars.insert("NATS_JETSTREAM".to_string(), "true".to_string());
let mut config = config;
config.env_vars = env_vars;
Self {
container: TestContainer::new(config),
}
}
pub async fn start(&mut self) -> Result<(), String> {
self.container.start().await
}
pub async fn stop(&mut self) -> Result<(), String> {
self.container.stop().await
}
pub fn connection_url(&self) -> String {
if let Some(port) = self.container.port(4222) {
format!("nats://{}:{}", self.container.host(), port)
} else {
"nats://localhost:4222".to_string()
}
}
}
#[derive(Debug)]
pub struct RedisContainer {
container: TestContainer,
}
impl RedisContainer {
pub fn new() -> Self {
let config = TestContainerConfig {
image: "redis:7".to_string(),
name: "test-redis".to_string(),
ports: vec![6379],
env_vars: std::collections::HashMap::new(),
remove_on_drop: true,
};
Self {
container: TestContainer::new(config),
}
}
pub async fn start(&mut self) -> Result<(), String> {
self.container.start().await
}
pub async fn stop(&mut self) -> Result<(), String> {
self.container.stop().await
}
pub fn connection_url(&self) -> String {
if let Some(port) = self.container.port(6379) {
format!("redis://{}:{}", self.container.host(), port)
} else {
"redis://localhost:6379".to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_test_container_config_default() {
let config = TestContainerConfig::default();
assert_eq!(config.image, "postgres:15");
assert_eq!(config.name, "test-container");
}
#[test]
fn test_postgres_container_connection_url() {
let container = PostgreSqlContainer::new();
let url = container.connection_url();
assert!(url.starts_with("postgres://"));
assert!(url.contains("testuser"));
assert!(url.contains("testdb"));
}
#[test]
fn test_nats_container_connection_url() {
let container = NatsContainer::new();
let url = container.connection_url();
assert!(url.starts_with("nats://"));
}
#[test]
fn test_redis_container_connection_url() {
let container = RedisContainer::new();
let url = container.connection_url();
assert!(url.starts_with("redis://"));
}
}