twc-rs 0.3.5

Fast single-binary CLI and interactive TUI dashboard for Timeweb Cloud
Documentation
use std::fs;

use serial_test::serial;

use super::*;

struct XdgGuard {
    original: Option<String>
}

impl XdgGuard {
    fn set(dir: &std::path::Path) -> Self {
        let original = std::env::var("XDG_CONFIG_HOME").ok();
        unsafe {
            std::env::set_var("XDG_CONFIG_HOME", dir);
        }
        Self {
            original
        }
    }
}

impl Drop for XdgGuard {
    fn drop(&mut self) {
        unsafe {
            match &self.original {
                Some(v) => std::env::set_var("XDG_CONFIG_HOME", v),
                None => std::env::remove_var("XDG_CONFIG_HOME")
            }
        }
    }
}

#[test]
fn default_config_has_no_token() {
    let cfg = AppConfig::default();
    assert!(cfg.token.is_none());
}

#[test]
fn path_returns_valid_toml_path() {
    let path = AppConfig::path().expect("config path should resolve");
    assert!(path.to_string_lossy().ends_with("config.toml"));
}

#[test]
fn path_contains_twc_rs_dir() {
    let path = AppConfig::path().expect("config path should resolve");
    let path_str = path.to_string_lossy();
    assert!(
        path_str.contains("twc-rs"),
        "Path should contain twc-rs dir: {path_str}"
    );
}

#[test]
#[serial]
fn load_returns_default_when_no_file() {
    let dir = tempfile::tempdir().unwrap();
    let _guard = XdgGuard::set(dir.path());
    let cfg = AppConfig::load().expect("load should succeed");
    assert!(cfg.token.is_none());
}

#[test]
#[serial]
fn load_returns_default_when_dir_exists_but_no_file() {
    let dir = tempfile::tempdir().unwrap();
    let config_dir = dir.path().join("twc-rs");
    fs::create_dir_all(&config_dir).unwrap();
    let _guard = XdgGuard::set(dir.path());
    let cfg = AppConfig::load().expect("load should succeed");
    assert!(cfg.token.is_none());
}

#[test]
#[serial]
fn load_parses_valid_toml_with_token() {
    let dir = tempfile::tempdir().unwrap();
    let config_dir = dir.path().join("twc-rs");
    fs::create_dir_all(&config_dir).unwrap();
    fs::write(
        config_dir.join("config.toml"),
        "token = \"my-secret-token\"\n"
    )
    .unwrap();
    let _guard = XdgGuard::set(dir.path());
    let cfg = AppConfig::load().expect("load should succeed");
    assert_eq!(cfg.token.as_deref(), Some("my-secret-token"));
}

#[test]
#[serial]
fn load_returns_config_parse_error_for_invalid_toml() {
    let dir = tempfile::tempdir().unwrap();
    let config_dir = dir.path().join("twc-rs");
    fs::create_dir_all(&config_dir).unwrap();
    fs::write(config_dir.join("config.toml"), "this is not [[valid toml").unwrap();
    let _guard = XdgGuard::set(dir.path());
    let err = AppConfig::load().unwrap_err();
    assert!(
        err.to_string().contains("failed to parse config"),
        "Expected parse error, got: {err}"
    );
}

#[test]
#[serial]
fn save_and_load_roundtrip() {
    let dir = tempfile::tempdir().unwrap();
    let _guard = XdgGuard::set(dir.path());
    let cfg = AppConfig {
        token: Some("roundtrip-token".to_string()),
        ..AppConfig::default()
    };
    cfg.save().expect("save should succeed");
    let loaded = AppConfig::load().expect("load should succeed");
    assert_eq!(loaded.token.as_deref(), Some("roundtrip-token"));
    let path = AppConfig::path().unwrap();
    assert!(path.exists());
}

#[test]
#[serial]
fn save_creates_parent_directories() {
    let dir = tempfile::tempdir().unwrap();
    let _guard = XdgGuard::set(dir.path());
    let cfg = AppConfig {
        token: Some("test".to_string()),
        ..AppConfig::default()
    };
    cfg.save().expect("save should succeed");
    let path = AppConfig::path().unwrap();
    assert!(path.parent().unwrap().exists());
}

#[test]
#[serial]
fn load_without_token_field() {
    let dir = tempfile::tempdir().unwrap();
    let config_dir = dir.path().join("twc-rs");
    fs::create_dir_all(&config_dir).unwrap();
    fs::write(config_dir.join("config.toml"), "").unwrap();
    let _guard = XdgGuard::set(dir.path());
    let cfg = AppConfig::load().expect("load should succeed");
    assert!(cfg.token.is_none());
}

#[test]
#[serial]
fn save_overwrites_existing_config() {
    let dir = tempfile::tempdir().unwrap();
    let _guard = XdgGuard::set(dir.path());
    let cfg1 = AppConfig {
        token: Some("first".to_string()),
        ..AppConfig::default()
    };
    cfg1.save().unwrap();
    let cfg2 = AppConfig {
        token: Some("second".to_string()),
        ..AppConfig::default()
    };
    cfg2.save().unwrap();
    let loaded = AppConfig::load().unwrap();
    assert_eq!(loaded.token.as_deref(), Some("second"));
}

#[test]
fn serialization_roundtrip_via_string() {
    let cfg = AppConfig {
        token: Some("my-secret-token".to_string()),
        ..AppConfig::default()
    };
    let toml_str = toml::to_string(&cfg).unwrap();
    let loaded: AppConfig = toml::from_str(&toml_str).unwrap();
    assert_eq!(loaded.token.as_deref(), Some("my-secret-token"));
}

#[test]
fn serialization_roundtrip_empty() {
    let cfg = AppConfig::default();
    let toml_str = toml::to_string(&cfg).unwrap();
    let loaded: AppConfig = toml::from_str(&toml_str).unwrap();
    assert!(loaded.token.is_none());
}

#[test]
fn deserialize_without_token_field() {
    let cfg: AppConfig = toml::from_str("").unwrap();
    assert!(cfg.token.is_none());
}

#[test]
fn deserialize_with_token() {
    let cfg: AppConfig = toml::from_str("token = \"abc123\"\n").unwrap();
    assert_eq!(cfg.token.as_deref(), Some("abc123"));
}

#[test]
fn deserialize_invalid_toml() {
    let result = toml::from_str::<AppConfig>("not [[valid");
    assert!(result.is_err());
}

#[test]
fn serialize_produces_valid_toml() {
    let cfg = AppConfig {
        token: Some("tok".to_string()),
        ..AppConfig::default()
    };
    let toml_str = toml::to_string(&cfg).unwrap();
    assert!(toml_str.contains("token"));
    assert!(toml_str.contains("tok"));
}

#[test]
fn serialize_skips_none_token() {
    let cfg = AppConfig::default();
    let toml_str = toml::to_string(&cfg).unwrap();
    assert!(!toml_str.contains("token"));
}

#[test]
fn save_and_load_roundtrip_filesystem() {
    let dir = tempfile::tempdir().unwrap();
    let file_path = dir.path().join("config.toml");
    let cfg = AppConfig {
        token: Some("roundtrip-token".to_string()),
        ..AppConfig::default()
    };
    let content = toml::to_string(&cfg).unwrap();
    fs::write(&file_path, content).unwrap();
    let read_back = fs::read_to_string(&file_path).unwrap();
    let loaded: AppConfig = toml::from_str(&read_back).unwrap();
    assert_eq!(loaded.token.as_deref(), Some("roundtrip-token"));
}

#[test]
fn save_creates_parent_directories_filesystem() {
    let dir = tempfile::tempdir().unwrap();
    let config_dir = dir.path().join("twc-rs");
    fs::create_dir_all(&config_dir).unwrap();
    let file_path = config_dir.join("config.toml");
    let cfg = AppConfig {
        token: Some("test".to_string()),
        ..AppConfig::default()
    };
    let content = toml::to_string(&cfg).unwrap();
    fs::write(&file_path, content).unwrap();
    assert!(file_path.exists());
    assert!(file_path.parent().unwrap().exists());
}

#[test]
fn load_from_filesystem_invalid_toml() {
    let dir = tempfile::tempdir().unwrap();
    let file_path = dir.path().join("config.toml");
    fs::write(&file_path, "this is not [[valid toml").unwrap();
    let content = fs::read_to_string(&file_path).unwrap();
    let result = toml::from_str::<AppConfig>(&content);
    assert!(result.is_err());
}

#[test]
fn deserialize_with_extra_fields_ignored() {
    let cfg: AppConfig = toml::from_str("token = \"abc\"\nother_field = true\n").unwrap();
    assert_eq!(cfg.token.as_deref(), Some("abc"));
}

#[test]
fn clone_config() {
    let cfg = AppConfig {
        token: Some("cloned".to_string()),
        ..AppConfig::default()
    };
    let cloned = cfg;
    assert_eq!(cloned.token.as_deref(), Some("cloned"));
}

#[test]
fn debug_format() {
    let cfg = AppConfig {
        token: Some("debug".to_string()),
        ..AppConfig::default()
    };
    let debug_str = format!("{cfg:?}");
    assert!(debug_str.contains("AppConfig"));
    assert!(debug_str.contains("debug"));
}