mcp-confluence 1.0.0

MCP server for Confluence integration - create, update, search, and manage Confluence pages
use std::env;

/// Confluence configuration loaded from environment variables.
#[derive(Clone, Debug)]
pub struct Config {
    pub host: String,
    #[allow(dead_code)]
    pub email: Option<String>,
    pub api_token: String,
    #[allow(dead_code)]
    pub api_version: String,
    pub is_cloud: bool,
    pub use_bearer: bool,
    pub max_content_length: usize,
}

/// Expand parameters for API optimization.
pub struct ExpandParams;
impl ExpandParams {
    pub const PAGE_CONTENT: &str = "body.storage,version";
    #[allow(dead_code)]
    pub const PAGE_WITH_ANCESTORS: &str = "body.storage,version,ancestors,space";
}

impl Config {
    /// Load configuration from environment variables.
    pub fn from_env() -> Result<Self, String> {
        let host = env::var("CONFLUENCE_HOST")
            .map(|h| h.trim_end_matches('/').to_string())
            .map_err(|_| "Missing CONFLUENCE_HOST")?;

        let api_token =
            env::var("CONFLUENCE_API_TOKEN").map_err(|_| "Missing CONFLUENCE_API_TOKEN")?;

        let email = env::var("CONFLUENCE_EMAIL").ok();

        let api_version = env::var("CONFLUENCE_API_VERSION").unwrap_or_else(|_| {
            if host.contains(".atlassian.net") {
                "2".to_string()
            } else {
                "1".to_string()
            }
        });

        let is_cloud = api_version == "2";

        let use_bearer =
            env::var("CONFLUENCE_USE_BEARER").map_or(!is_cloud, |v| v == "true" || !is_cloud);

        if is_cloud && !use_bearer && email.is_none() {
            return Err("Missing CONFLUENCE_EMAIL (required for Confluence Cloud)".to_string());
        }

        let max_content_length = env::var("CONFLUENCE_MAX_CONTENT_LENGTH")
            .ok()
            .and_then(|v| v.parse().ok())
            .unwrap_or(30000);

        Ok(Config {
            host,
            email,
            api_token,
            api_version,
            is_cloud,
            use_bearer,
            max_content_length,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Mutex;

    // Env-var-based tests must run serially to avoid interference.
    static ENV_LOCK: Mutex<()> = Mutex::new(());

    fn clear_confluence_env() {
        for key in [
            "CONFLUENCE_HOST",
            "CONFLUENCE_API_TOKEN",
            "CONFLUENCE_EMAIL",
            "CONFLUENCE_API_VERSION",
            "CONFLUENCE_USE_BEARER",
            "CONFLUENCE_MAX_CONTENT_LENGTH",
        ] {
            env::remove_var(key);
        }
    }

    #[test]
    fn missing_host() {
        let _lock = ENV_LOCK.lock().unwrap();
        clear_confluence_env();
        env::set_var("CONFLUENCE_API_TOKEN", "tok");
        let err = Config::from_env().unwrap_err();
        assert!(err.contains("CONFLUENCE_HOST"));
    }

    #[test]
    fn missing_token() {
        let _lock = ENV_LOCK.lock().unwrap();
        clear_confluence_env();
        env::set_var("CONFLUENCE_HOST", "https://example.com");
        let err = Config::from_env().unwrap_err();
        assert!(err.contains("CONFLUENCE_API_TOKEN"));
    }

    #[test]
    fn server_dc_defaults() {
        let _lock = ENV_LOCK.lock().unwrap();
        clear_confluence_env();
        env::set_var("CONFLUENCE_HOST", "https://confluence.corp.com");
        env::set_var("CONFLUENCE_API_TOKEN", "my-token");
        let cfg = Config::from_env().unwrap();
        assert_eq!(cfg.api_version, "1");
        assert!(!cfg.is_cloud);
        assert!(cfg.use_bearer); // on-prem defaults to bearer
        assert_eq!(cfg.max_content_length, 30000);
    }

    #[test]
    fn cloud_auto_detection() {
        let _lock = ENV_LOCK.lock().unwrap();
        clear_confluence_env();
        env::set_var("CONFLUENCE_HOST", "https://mysite.atlassian.net");
        env::set_var("CONFLUENCE_API_TOKEN", "token");
        env::set_var("CONFLUENCE_EMAIL", "user@example.com");
        let cfg = Config::from_env().unwrap();
        assert_eq!(cfg.api_version, "2");
        assert!(cfg.is_cloud);
        assert!(!cfg.use_bearer); // cloud defaults to basic auth
    }

    #[test]
    fn cloud_missing_email() {
        let _lock = ENV_LOCK.lock().unwrap();
        clear_confluence_env();
        env::set_var("CONFLUENCE_HOST", "https://mysite.atlassian.net");
        env::set_var("CONFLUENCE_API_TOKEN", "token");
        let err = Config::from_env().unwrap_err();
        assert!(err.contains("CONFLUENCE_EMAIL"));
    }

    #[test]
    fn host_trailing_slash_stripped() {
        let _lock = ENV_LOCK.lock().unwrap();
        clear_confluence_env();
        env::set_var("CONFLUENCE_HOST", "https://example.com/");
        env::set_var("CONFLUENCE_API_TOKEN", "tok");
        let cfg = Config::from_env().unwrap();
        assert_eq!(cfg.host, "https://example.com");
    }

    #[test]
    fn custom_max_content_length() {
        let _lock = ENV_LOCK.lock().unwrap();
        clear_confluence_env();
        env::set_var("CONFLUENCE_HOST", "https://example.com");
        env::set_var("CONFLUENCE_API_TOKEN", "tok");
        env::set_var("CONFLUENCE_MAX_CONTENT_LENGTH", "50000");
        let cfg = Config::from_env().unwrap();
        assert_eq!(cfg.max_content_length, 50000);
    }

    #[test]
    fn invalid_max_content_length_falls_back() {
        let _lock = ENV_LOCK.lock().unwrap();
        clear_confluence_env();
        env::set_var("CONFLUENCE_HOST", "https://example.com");
        env::set_var("CONFLUENCE_API_TOKEN", "tok");
        env::set_var("CONFLUENCE_MAX_CONTENT_LENGTH", "not-a-number");
        let cfg = Config::from_env().unwrap();
        assert_eq!(cfg.max_content_length, 30000);
    }

    #[test]
    fn explicit_api_version() {
        let _lock = ENV_LOCK.lock().unwrap();
        clear_confluence_env();
        env::set_var("CONFLUENCE_HOST", "https://example.com");
        env::set_var("CONFLUENCE_API_TOKEN", "tok");
        env::set_var("CONFLUENCE_API_VERSION", "2");
        env::set_var("CONFLUENCE_EMAIL", "a@b.com");
        let cfg = Config::from_env().unwrap();
        assert_eq!(cfg.api_version, "2");
        assert!(cfg.is_cloud);
    }

    #[test]
    fn expand_params_constants() {
        assert_eq!(ExpandParams::PAGE_CONTENT, "body.storage,version");
        assert_eq!(
            ExpandParams::PAGE_WITH_ANCESTORS,
            "body.storage,version,ancestors,space"
        );
    }
}