sunbeam-g2v 0.3.3

Sunbeam Service Framework - A ConnectRPC-based framework for building microservices
Documentation
//! Test containers support for Sunbeam services.

/// Test container configuration.
#[derive(Debug, Clone)]
pub struct TestContainerConfig {
    /// Container image.
    pub image: String,
    /// Container name.
    pub name: String,
    /// Exposed ports.
    pub ports: Vec<u16>,
    /// Environment variables.
    pub env_vars: std::collections::HashMap<String, String>,
    /// Whether to remove the container on drop.
    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,
        }
    }
}

/// Test container.
/// 
/// This is a placeholder for testcontainers integration.
/// In a real implementation, this would use the testcontainers crate
/// to manage Docker containers for integration testing.
#[derive(Debug)]
pub struct TestContainer {
    /// The configuration.
    config: TestContainerConfig,
    /// Whether the container is running.
    is_running: bool,
}

impl TestContainer {
    /// Create a new test container with the given configuration.
    pub fn new(config: TestContainerConfig) -> Self {
        Self {
            config,
            is_running: false,
        }
    }

    /// Get the configuration.
    pub fn config(&self) -> &TestContainerConfig {
        &self.config
    }

    /// Check if the container is running.
    pub fn is_running(&self) -> bool {
        self.is_running
    }

    /// Start the container.
    pub async fn start(&mut self) -> Result<(), String> {
        // In a real implementation:
        // use testcontainers::clients::Cli;
        // use testcontainers::Container;
        // let docker = Cli::default();
        // let container = docker.run(self.config.image.clone());
        // self.container = Some(container);
        // self.is_running = true;

        log::info!("Starting test container: {}", self.config.name);
        self.is_running = true;
        Ok(())
    }

    /// Stop the container.
    pub async fn stop(&mut self) -> Result<(), String> {
        // In a real implementation:
        // if let Some(ref container) = self.container {
        //     container.stop();
        // }

        log::info!("Stopping test container: {}", self.config.name);
        self.is_running = false;
        Ok(())
    }

    /// Get the container's host.
    pub fn host(&self) -> String {
        "localhost".to_string()
    }

    /// Get the container's port for a service.
    pub fn port(&self, service_port: u16) -> Option<u16> {
        // In a real implementation, this would map the container's
        // internal port to the host port
        if self.config.ports.contains(&service_port) {
            Some(service_port)
        } else {
            None
        }
    }

    /// Get the connection URL for a service.
    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 {
            // In a real implementation, stop the container
            // For now, just log
            log::info!("Dropping test container: {}", self.config.name);
        }
    }
}

/// PostgreSQL test container.
#[derive(Debug)]
pub struct PostgreSqlContainer {
    /// The underlying test container.
    container: TestContainer,
    /// Database name.
    database: String,
    /// Username.
    username: String,
    /// Password.
    password: String,
}

impl Default for PostgreSqlContainer {
    fn default() -> Self {
        Self::new()
    }
}

impl PostgreSqlContainer {
    /// Create a new PostgreSQL test container.
    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(),
        }
    }

    /// Start the container.
    pub async fn start(&mut self) -> Result<(), String> {
        self.container.start().await
    }

    /// Stop the container.
    pub async fn stop(&mut self) -> Result<(), String> {
        self.container.stop().await
    }

    /// Get the connection URL.
    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
            )
        }
    }
}

/// NATS test container.
#[derive(Debug)]
pub struct NatsContainer {
    /// The underlying test container.
    container: TestContainer,
}

impl Default for NatsContainer {
    fn default() -> Self {
        Self::new()
    }
}

impl NatsContainer {
    /// Create a new NATS test container.
    pub fn new() -> Self {
        let mut 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,
        };

        // Enable JetStream
        config.env_vars.insert("NATS_JETSTREAM".to_string(), "true".to_string());

        Self {
            container: TestContainer::new(config),
        }
    }

    /// Start the container.
    pub async fn start(&mut self) -> Result<(), String> {
        self.container.start().await
    }

    /// Stop the container.
    pub async fn stop(&mut self) -> Result<(), String> {
        self.container.stop().await
    }

    /// Get the connection URL.
    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()
        }
    }
}

/// Redis test container.
#[derive(Debug)]
pub struct RedisContainer {
    /// The underlying test container.
    container: TestContainer,
}

impl Default for RedisContainer {
    fn default() -> Self {
        Self::new()
    }
}

impl RedisContainer {
    /// Create a new Redis test container.
    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),
        }
    }

    /// Start the container.
    pub async fn start(&mut self) -> Result<(), String> {
        self.container.start().await
    }

    /// Stop the container.
    pub async fn stop(&mut self) -> Result<(), String> {
        self.container.stop().await
    }

    /// Get the connection URL.
    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://"));
    }
}