use crate::core::file_error::{FileOperation, FileResultExt};
use anyhow::Result;
use clap::{Args, Subcommand};
use colored::Colorize;
use std::path::PathBuf;
use crate::config::GlobalConfig;
#[derive(Args)]
pub struct ConfigCommand {
#[command(subcommand)]
command: Option<ConfigSubcommands>,
}
#[derive(Subcommand)]
enum ConfigSubcommands {
Init {
#[arg(long)]
force: bool,
},
Show,
Edit,
AddSource {
name: String,
url: String,
},
RemoveSource {
name: String,
},
ListSources,
Path,
}
impl ConfigCommand {
pub async fn execute(self, config_path: Option<PathBuf>) -> Result<()> {
match self.command {
Some(ConfigSubcommands::Init {
force,
}) => Self::init_with_config_path(force, config_path).await,
Some(ConfigSubcommands::Show) | None => Self::show(config_path).await,
Some(ConfigSubcommands::Edit) => Self::edit_with_path(config_path).await,
Some(ConfigSubcommands::AddSource {
name,
url,
}) => Self::add_source_with_path(name, url, config_path).await,
Some(ConfigSubcommands::RemoveSource {
name,
}) => Self::remove_source_with_path(name, config_path).await,
Some(ConfigSubcommands::ListSources) => Self::list_sources_with_path(config_path).await,
Some(ConfigSubcommands::Path) => {
Self::show_path(config_path);
Ok(())
}
}
}
async fn init_with_config_path(force: bool, config_path: Option<PathBuf>) -> Result<()> {
let config_path = config_path.unwrap_or_else(|| {
GlobalConfig::default_path().unwrap_or_else(|_| PathBuf::from("~/.agpm/config.toml"))
});
if config_path.exists() && !force {
println!("❌ Global config already exists at: {}", config_path.display());
println!(" Use --force to overwrite");
return Ok(());
}
let config = GlobalConfig::init_example();
if let Some(parent) = config_path.parent() {
tokio::fs::create_dir_all(parent).await.with_file_context(
FileOperation::CreateDir,
parent,
"creating parent directory for config",
"cli_config",
)?;
}
config.save_to(&config_path).await?;
println!("✅ Created global config at: {}", config_path.display());
println!("\n{}", "Example configuration:".bold());
println!("{}", toml::to_string_pretty(&config)?);
println!("\n{}", "Next steps:".yellow());
println!(" 1. Edit the config to add your private sources with authentication");
println!(" 2. Replace 'YOUR_TOKEN' with actual access tokens");
Ok(())
}
#[allow(dead_code)] pub async fn init_with_path(force: bool, base_dir: Option<PathBuf>) -> Result<()> {
let config_path = if let Some(base) = base_dir {
base.join("config.toml")
} else {
GlobalConfig::default_path()?
};
if config_path.exists() && !force {
println!("❌ Global config already exists at: {}", config_path.display());
println!(" Use --force to overwrite");
return Ok(());
}
let config = GlobalConfig::init_example();
if let Some(parent) = config_path.parent() {
tokio::fs::create_dir_all(parent).await.with_file_context(
FileOperation::CreateDir,
parent,
"creating parent directory for config",
"cli_config",
)?;
}
config.save_to(&config_path).await?;
println!("✅ Created global config at: {}", config_path.display());
println!("\n{}", "Example configuration:".bold());
println!("{}", toml::to_string_pretty(&config)?);
println!("\n{}", "Next steps:".yellow());
println!(" 1. Edit the config to add your private sources with authentication");
println!(" 2. Replace 'YOUR_TOKEN' with actual access tokens");
Ok(())
}
async fn show(config_path: Option<PathBuf>) -> Result<()> {
let config = GlobalConfig::load_with_optional(config_path.clone()).await?;
let config_path = config_path.unwrap_or_else(|| {
GlobalConfig::default_path().unwrap_or_else(|_| PathBuf::from("~/.agpm/config.toml"))
});
println!("{}", "Global Configuration".bold());
println!("Location: {}\n", config_path.display());
if config.sources.is_empty() {
println!("No global sources configured.");
println!("\n{}", "Tip:".yellow());
println!(" Run 'agpm config init' to create an example configuration");
} else {
println!("{}", toml::to_string_pretty(&config)?);
}
Ok(())
}
async fn edit_with_path(config_path: Option<PathBuf>) -> Result<()> {
let config_path = config_path.unwrap_or_else(|| {
GlobalConfig::default_path().unwrap_or_else(|_| PathBuf::from("~/.agpm/config.toml"))
});
if !config_path.exists() {
println!("❌ No global config found. Creating one...");
let config = GlobalConfig::init_example();
config.save().await?;
}
let editor =
std::env::var("EDITOR").or_else(|_| std::env::var("VISUAL")).unwrap_or_else(|_| {
if cfg!(target_os = "windows") {
"notepad".to_string()
} else {
"vi".to_string()
}
});
println!("Opening {} in {}...", config_path.display(), editor);
let status = std::process::Command::new(&editor).arg(&config_path).status()?;
if status.success() {
println!("✅ Config edited successfully");
} else {
println!("❌ Editor exited with error");
}
Ok(())
}
async fn add_source_with_path(
name: String,
url: String,
config_path: Option<PathBuf>,
) -> Result<()> {
let mut config =
GlobalConfig::load_with_optional(config_path.clone()).await.unwrap_or_default();
if config.has_source(&name) {
println!("⚠️ Source '{name}' already exists");
println!(" Current URL: {}", config.get_source(&name).unwrap());
println!(" New URL: {url}");
println!(" Updating...");
}
config.add_source(name.clone(), url.clone());
let save_path = config_path.unwrap_or_else(|| {
GlobalConfig::default_path().unwrap_or_else(|_| PathBuf::from("~/.agpm/config.toml"))
});
config.save_to(&save_path).await?;
println!("✅ Added global source '{}': {}", name.green(), url);
if url.contains("YOUR_TOKEN") || url.contains("TOKEN") {
println!("\n{}", "Warning:".yellow());
println!(" Remember to replace 'YOUR_TOKEN' with an actual access token");
}
Ok(())
}
async fn remove_source_with_path(name: String, config_path: Option<PathBuf>) -> Result<()> {
let mut config =
GlobalConfig::load_with_optional(config_path.clone()).await.unwrap_or_default();
if config.remove_source(&name) {
let save_path = config_path.unwrap_or_else(|| {
GlobalConfig::default_path()
.unwrap_or_else(|_| PathBuf::from("~/.agpm/config.toml"))
});
config.save_to(&save_path).await?;
println!("✅ Removed global source '{}'", name.red());
} else {
println!("❌ Source '{name}' not found in global config");
}
Ok(())
}
async fn list_sources_with_path(config_path: Option<PathBuf>) -> Result<()> {
let config = GlobalConfig::load_with_optional(config_path).await.unwrap_or_default();
if config.sources.is_empty() {
println!("No global sources configured.");
println!("\n{}", "Tip:".yellow());
println!(" Add a source with: agpm config add-source <name> <url>");
println!(
" Example: agpm config add-source private https://oauth2:TOKEN@gitlab.com/company/agents.git"
);
} else {
println!("{}", "Global Sources:".bold());
for (name, url) in &config.sources {
let display_url = if url.contains('@') {
let parts: Vec<&str> = url.splitn(2, '@').collect();
if parts.len() == 2 {
let auth_parts: Vec<&str> = parts[0].rsplitn(2, '/').collect();
if auth_parts.len() == 2 {
format!("{}//***@{}", auth_parts[1], parts[1])
} else {
url.clone()
}
} else {
url.clone()
}
} else {
url.clone()
};
println!(" {} → {}", name.cyan(), display_url);
}
}
Ok(())
}
fn show_path(config_path: Option<PathBuf>) {
let config_path = config_path.unwrap_or_else(|| {
GlobalConfig::default_path().unwrap_or_else(|_| PathBuf::from("~/.agpm/config.toml"))
});
println!("{}", config_path.display());
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_config_path() -> Result<()> {
ConfigCommand::show_path(None);
Ok(())
}
#[tokio::test]
async fn test_config_init() -> Result<()> {
let temp = TempDir::new().unwrap();
let base_dir = temp.path().to_path_buf();
ConfigCommand::init_with_path(false, Some(base_dir.clone())).await?;
let config_path = base_dir.join("config.toml");
assert!(config_path.exists());
ConfigCommand::init_with_path(false, Some(base_dir.clone())).await?;
ConfigCommand::init_with_path(true, Some(base_dir.clone())).await?;
Ok(())
}
#[tokio::test]
async fn test_config_show_empty() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
ConfigCommand::show(Some(config_path)).await?;
Ok(())
}
#[tokio::test]
async fn test_config_add_source() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let mut config = GlobalConfig::default();
config.add_source(
"private".to_string(),
"https://oauth2:TOKEN@github.com/org/repo.git".to_string(),
);
assert!(config.has_source("private"));
assert_eq!(
config.get_source("private"),
Some(&"https://oauth2:TOKEN@github.com/org/repo.git".to_string())
);
config.add_source(
"private".to_string(),
"https://oauth2:NEW_TOKEN@github.com/org/repo.git".to_string(),
);
assert_eq!(
config.get_source("private"),
Some(&"https://oauth2:NEW_TOKEN@github.com/org/repo.git".to_string())
);
config.save_to(&config_path).await?;
let loaded_config = GlobalConfig::load_from(&config_path).await?;
assert!(loaded_config.has_source("private"));
Ok(())
}
#[tokio::test]
async fn test_config_remove_source() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let mut config = GlobalConfig::default();
config.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
config.add_source("keep".to_string(), "https://github.com/keep/repo.git".to_string());
assert!(config.remove_source("test"));
assert!(!config.has_source("test"));
assert!(config.has_source("keep"));
assert!(!config.remove_source("nonexistent"));
config.save_to(&config_path).await?;
let loaded_config = GlobalConfig::load_from(&config_path).await?;
assert!(!loaded_config.has_source("test"));
assert!(loaded_config.has_source("keep"));
Ok(())
}
#[tokio::test]
async fn test_config_list_sources() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let empty_config = GlobalConfig::default();
assert!(empty_config.sources.is_empty());
let mut config = GlobalConfig::default();
config.add_source("public".to_string(), "https://github.com/org/public.git".to_string());
config.add_source(
"private".to_string(),
"https://oauth2:token@github.com/org/private.git".to_string(),
);
assert_eq!(config.sources.len(), 2);
assert!(config.has_source("public"));
assert!(config.has_source("private"));
config.save_to(&config_path).await?;
let loaded_config = GlobalConfig::load_from(&config_path).await?;
assert_eq!(loaded_config.sources.len(), 2);
Ok(())
}
#[tokio::test]
async fn test_config_subcommands() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let mut config = GlobalConfig::init_example();
assert!(config.has_source("private"));
assert!(config.get_source("private").unwrap().contains("YOUR_TOKEN"));
config.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
assert!(config.has_source("test"));
assert!(config.remove_source("test"));
assert!(!config.has_source("test"));
config.save_to(&config_path).await?;
assert!(config_path.exists());
let loaded = GlobalConfig::load_from(&config_path).await?;
assert_eq!(loaded.sources.len(), config.sources.len());
Ok(())
}
#[test]
fn test_url_token_masking() {
let url = "https://oauth2:ghp_123456@github.com/org/repo.git";
let masked = if url.contains('@') {
let parts: Vec<&str> = url.splitn(2, '@').collect();
if parts.len() == 2 {
let auth_parts: Vec<&str> = parts[0].rsplitn(2, '/').collect();
if auth_parts.len() == 2 {
format!("{}//***@{}", auth_parts[1], parts[1])
} else {
url.to_string()
}
} else {
url.to_string()
}
} else {
url.to_string()
};
assert_eq!(masked, "https:///***@github.com/org/repo.git");
let url = "https://github.com/org/repo.git";
let masked = if url.contains('@') {
"masked".to_string()
} else {
url.to_string()
};
assert_eq!(masked, url);
}
#[tokio::test]
async fn test_config_execute_init() -> Result<()> {
let cmd = ConfigCommand {
command: Some(ConfigSubcommands::Init {
force: false,
}),
};
let _ = cmd;
Ok(())
}
#[tokio::test]
async fn test_config_execute_show() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let config = GlobalConfig::default();
config.save_to(&config_path).await?;
ConfigCommand::show_path(None); Ok(())
}
#[tokio::test]
async fn test_config_add_and_remove_source() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let mut config = GlobalConfig::default();
config
.add_source("test-source".to_string(), "https://github.com/test/repo.git".to_string());
assert!(config.has_source("test-source"));
assert_eq!(
config.get_source("test-source"),
Some(&"https://github.com/test/repo.git".to_string())
);
config.save_to(&config_path).await?;
let loaded = GlobalConfig::load_from(&config_path).await?;
assert!(loaded.has_source("test-source"));
let mut config = loaded;
config.remove_source("test-source");
assert!(!config.has_source("test-source"));
config.save_to(&config_path).await?;
let loaded = GlobalConfig::load_from(&config_path).await?;
assert!(!loaded.has_source("test-source"));
Ok(())
}
#[tokio::test]
async fn test_config_list_sources_empty() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let config = GlobalConfig::default();
config.save_to(&config_path).await?;
let loaded = GlobalConfig::load_from(&config_path).await?;
assert_eq!(loaded.sources.len(), 0);
Ok(())
}
#[tokio::test]
async fn test_config_list_sources_with_multiple() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let mut config = GlobalConfig::default();
config.add_source("source1".to_string(), "https://github.com/org/repo1.git".to_string());
config.add_source(
"source2".to_string(),
"https://oauth2:token@github.com/org/repo2.git".to_string(),
);
config.save_to(&config_path).await?;
let loaded = GlobalConfig::load_from(&config_path).await?;
assert_eq!(loaded.sources.len(), 2);
assert!(loaded.has_source("source1"));
assert!(loaded.has_source("source2"));
Ok(())
}
#[tokio::test]
async fn test_config_execute_default_to_show() -> Result<()> {
let cmd = ConfigCommand {
command: None, };
assert!(cmd.command.is_none());
Ok(())
}
#[tokio::test]
async fn test_config_execute_path_subcommand() -> Result<()> {
let cmd = ConfigCommand {
command: Some(ConfigSubcommands::Path),
};
cmd.execute(None).await?;
Ok(())
}
#[tokio::test]
async fn test_config_execute_init_subcommand() -> Result<()> {
let temp = TempDir::new().unwrap();
ConfigCommand::init_with_path(false, Some(temp.path().to_path_buf())).await?;
let config_path = temp.path().join("config.toml");
assert!(config_path.exists());
Ok(())
}
#[tokio::test]
async fn test_init_method_wrapper() -> Result<()> {
let temp = TempDir::new().unwrap();
ConfigCommand::init_with_path(false, Some(temp.path().to_path_buf())).await?;
let config_path = temp.path().join("config.toml");
assert!(config_path.exists());
ConfigCommand::init_with_path(true, Some(temp.path().to_path_buf())).await?;
Ok(())
}
#[tokio::test]
async fn test_show_with_populated_config() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let mut config = GlobalConfig::default();
config.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
config.save_to(&config_path).await?;
let loaded = GlobalConfig::load_from(&config_path).await?;
assert!(!loaded.sources.is_empty());
assert!(loaded.has_source("test"));
Ok(())
}
#[tokio::test]
async fn test_edit_method_config_creation() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
assert!(!config_path.exists());
let config = GlobalConfig::init_example();
config.save_to(&config_path).await?;
assert!(config_path.exists());
let loaded = GlobalConfig::load_from(&config_path).await?;
assert!(!loaded.sources.is_empty());
Ok(())
}
#[tokio::test]
async fn test_add_source_comprehensive() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let mut config = GlobalConfig::default();
assert!(config.sources.is_empty());
config.add_source("first".to_string(), "https://github.com/org/repo.git".to_string());
assert!(config.has_source("first"));
assert_eq!(config.sources.len(), 1);
config.add_source(
"with-token".to_string(),
"https://oauth2:YOUR_TOKEN@github.com/org/private.git".to_string(),
);
assert!(config.has_source("with-token"));
assert_eq!(config.sources.len(), 2);
let url = config.get_source("with-token").unwrap();
assert!(url.contains("YOUR_TOKEN"));
let original_url = config.get_source("first").unwrap().clone();
config
.add_source("first".to_string(), "https://github.com/org/updated-repo.git".to_string());
let updated_url = config.get_source("first").unwrap();
assert_ne!(original_url, *updated_url);
assert_eq!(updated_url, "https://github.com/org/updated-repo.git");
assert_eq!(config.sources.len(), 2);
config.save_to(&config_path).await?;
let loaded = GlobalConfig::load_from(&config_path).await?;
assert_eq!(loaded.sources.len(), 2);
assert!(loaded.has_source("first"));
assert!(loaded.has_source("with-token"));
Ok(())
}
#[tokio::test]
async fn test_remove_source_comprehensive() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let mut config = GlobalConfig::default();
config.add_source("first".to_string(), "https://github.com/org/repo1.git".to_string());
config.add_source("second".to_string(), "https://github.com/org/repo2.git".to_string());
config.add_source("third".to_string(), "https://github.com/org/repo3.git".to_string());
assert_eq!(config.sources.len(), 3);
assert!(config.remove_source("second"));
assert_eq!(config.sources.len(), 2);
assert!(!config.has_source("second"));
assert!(config.has_source("first"));
assert!(config.has_source("third"));
assert!(!config.remove_source("nonexistent"));
assert_eq!(config.sources.len(), 2);
assert!(config.remove_source("first"));
assert_eq!(config.sources.len(), 1);
assert!(!config.has_source("first"));
assert!(config.has_source("third"));
assert!(config.remove_source("third"));
assert_eq!(config.sources.len(), 0);
assert!(config.sources.is_empty());
config.save_to(&config_path).await?;
let loaded = GlobalConfig::load_from(&config_path).await?;
assert!(loaded.sources.is_empty());
Ok(())
}
#[tokio::test]
async fn test_list_sources_comprehensive() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let empty_config = GlobalConfig::default();
assert!(empty_config.sources.is_empty());
let mut config = GlobalConfig::default();
config
.add_source("public".to_string(), "https://github.com/org/public-repo.git".to_string());
config.add_source(
"oauth".to_string(),
"https://oauth2:ghp_1234567890abcdef@github.com/org/private.git".to_string(),
);
config.add_source(
"usertoken".to_string(),
"https://username:secret_token@gitlab.com/group/repo.git".to_string(),
);
config.add_source(
"placeholder".to_string(),
"https://oauth2:TOKEN@github.com/company/resources.git".to_string(),
);
config.add_source("ssh".to_string(), "git@github.com:org/repo.git".to_string());
assert_eq!(config.sources.len(), 5);
let urls_to_test = vec![
("https://github.com/org/public-repo.git", "https://github.com/org/public-repo.git"), (
"https://oauth2:ghp_1234567890abcdef@github.com/org/private.git",
"https://***@github.com/org/private.git",
),
(
"https://username:secret_token@gitlab.com/group/repo.git",
"https://***@gitlab.com/group/repo.git",
),
("git@github.com:org/repo.git", "git@github.com:org/repo.git"), ];
for (original_url, _expected_masked) in urls_to_test {
let masked = if original_url.contains('@') && original_url.starts_with("https://") {
let parts: Vec<&str> = original_url.splitn(2, '@').collect();
if parts.len() == 2 {
let auth_parts: Vec<&str> = parts[0].rsplitn(2, '/').collect();
if auth_parts.len() == 2 {
format!("{}//***@{}", auth_parts[1], parts[1])
} else {
original_url.to_string()
}
} else {
original_url.to_string()
}
} else {
original_url.to_string()
};
if original_url.contains('@') && original_url.starts_with("https://") {
assert!(masked.contains("***"));
assert!(!masked.contains("ghp_"));
assert!(!masked.contains("secret_token"));
}
}
config.save_to(&config_path).await?;
let loaded = GlobalConfig::load_from(&config_path).await?;
assert_eq!(loaded.sources.len(), 5);
for name in &["public", "oauth", "usertoken", "placeholder", "ssh"] {
assert!(loaded.has_source(name), "Missing source: {name}");
}
Ok(())
}
#[test]
fn test_url_masking_edge_cases() {
let test_cases = vec![
("https://oauth2:token@github.com/org/repo.git", true),
("https://user:pass@gitlab.com/group/repo.git", true),
("https://github.com/org/repo.git", false),
("git@github.com:org/repo.git", true), ("https://@github.com/org/repo.git", true), ("https://token@github.com/org/repo.git", true), ("ftp://user:pass@example.com/repo", true), ("https://github.com/@org/repo.git", true), ("", false), ];
for (url, has_at) in test_cases {
assert_eq!(url.contains('@'), has_at, "Failed for URL: {url}");
if url.contains('@') {
let parts: Vec<&str> = url.splitn(2, '@').collect();
if parts.len() == 2 {
let auth_parts: Vec<&str> = parts[0].rsplitn(2, '/').collect();
if auth_parts.len() == 2 {
let masked = format!("{}//***@{}", auth_parts[1], parts[1]);
assert!(masked.contains("***"));
assert!(!masked.is_empty());
}
}
}
}
}
#[test]
fn test_token_warning_patterns() {
let warning_urls = vec![
"https://oauth2:YOUR_TOKEN@github.com/org/repo.git",
"https://user:TOKEN@gitlab.com/group/repo.git",
"https://TOKEN@bitbucket.org/workspace/repo.git",
"https://oauth2:ghp_YOUR_TOKEN@github.com/company/private.git",
];
let non_warning_urls = vec![
"https://oauth2:ghp_real_token_123@github.com/org/repo.git",
"https://github.com/org/public.git",
"git@github.com:org/repo.git",
"https://user:actual_secret@gitlab.com/group/repo.git",
];
for url in warning_urls {
assert!(
url.contains("YOUR_TOKEN") || url.contains("TOKEN"),
"URL should trigger warning: {url}"
);
}
for url in non_warning_urls {
assert!(
!(url.contains("YOUR_TOKEN") || (url.contains("TOKEN") && !url.contains("actual"))),
"URL should not trigger warning: {url}"
);
}
}
#[tokio::test]
async fn test_config_file_operations() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let empty_config = GlobalConfig::default();
empty_config.save_to(&config_path).await?;
assert!(config_path.exists());
let loaded_config = GlobalConfig::load_from(&config_path).await?;
assert!(loaded_config.sources.is_empty());
let mut config_with_sources = GlobalConfig::default();
config_with_sources
.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
config_with_sources.save_to(&config_path).await?;
let loaded_config = GlobalConfig::load_from(&config_path).await?;
assert_eq!(loaded_config.sources.len(), 1);
assert!(loaded_config.has_source("test"));
Ok(())
}
#[tokio::test]
async fn test_execute_add_source_command() -> Result<()> {
let cmd = ConfigCommand {
command: Some(ConfigSubcommands::AddSource {
name: "test-source".to_string(),
url: "https://github.com/test/repo.git".to_string(),
}),
};
let _ = cmd.execute(None).await;
Ok(())
}
#[tokio::test]
async fn test_execute_remove_source_command() -> Result<()> {
let cmd = ConfigCommand {
command: Some(ConfigSubcommands::RemoveSource {
name: "nonexistent".to_string(),
}),
};
let _ = cmd.execute(None).await;
Ok(())
}
#[tokio::test]
async fn test_execute_list_sources_command() -> Result<()> {
let cmd = ConfigCommand {
command: Some(ConfigSubcommands::ListSources),
};
cmd.execute(None).await?;
Ok(())
}
#[tokio::test]
async fn test_execute_path_command() -> Result<()> {
let cmd = ConfigCommand {
command: Some(ConfigSubcommands::Path),
};
cmd.execute(None).await?;
Ok(())
}
#[tokio::test]
async fn test_execute_edit_command() -> Result<()> {
let cmd = ConfigCommand {
command: Some(ConfigSubcommands::Edit),
};
assert!(matches!(cmd.command, Some(ConfigSubcommands::Edit)));
Ok(())
}
#[test]
fn test_url_token_masking_comprehensive() {
let test_cases = vec![
("https://oauth2:ghp_xxx@github.com/org/repo.git", true),
("https://gitlab-ci-token:abc123@gitlab.com/group/repo.git", true),
("https://username:password@bitbucket.org/team/repo.git", true),
("ssh://git@github.com:org/repo.git", false), ("https://github.com/org/repo.git", false), ("git@github.com:org/repo.git", false), ("https://token@dev.azure.com/org/project/_git/repo", true),
("https://@github.com/org/repo.git", false), ];
for (url, should_mask) in test_cases {
if should_mask {
assert!(url.contains('@'), "URL should contain @ for masking: {}", url);
}
}
}
#[tokio::test]
async fn test_init_force_overwrite() -> Result<()> {
let temp = TempDir::new().unwrap();
let base_dir = temp.path().to_path_buf();
ConfigCommand::init_with_path(false, Some(base_dir.clone())).await?;
let config_path = base_dir.join("config.toml");
let initial_content = std::fs::read_to_string(&config_path).unwrap();
ConfigCommand::init_with_path(true, Some(base_dir.clone())).await?;
let new_content = std::fs::read_to_string(&config_path).unwrap();
assert_eq!(initial_content, new_content); Ok(())
}
#[tokio::test]
async fn test_add_source_with_warning_tokens() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let mut config = GlobalConfig::default();
config.add_source(
"test".to_string(),
"https://oauth2:YOUR_TOKEN@github.com/org/repo.git".to_string(),
);
assert!(config.get_source("test").unwrap().contains("YOUR_TOKEN"));
config.save_to(&config_path).await?;
assert!(config_path.exists());
Ok(())
}
#[tokio::test]
async fn test_remove_nonexistent_source() -> Result<()> {
let mut config = GlobalConfig::default();
config.add_source("exists".to_string(), "https://github.com/test/repo.git".to_string());
assert!(config.remove_source("exists"));
assert!(!config.remove_source("doesnt_exist"));
assert!(!config.remove_source("never_existed"));
Ok(())
}
#[tokio::test]
async fn test_list_sources_url_masking() -> Result<()> {
let test_urls = vec![
"https://oauth2:secret@github.com/org/repo.git",
"https://user:pass@gitlab.com/group/repo.git",
"ssh://git@github.com:org/repo.git",
"https://github.com/org/repo.git",
];
for url in test_urls {
if url.contains('@') {
let parts: Vec<&str> = url.splitn(2, '@').collect();
assert_eq!(parts.len(), 2);
}
}
Ok(())
}
#[tokio::test]
async fn test_show_empty_vs_populated() -> Result<()> {
let empty_config = GlobalConfig::default();
assert!(empty_config.sources.is_empty());
let mut populated_config = GlobalConfig::default();
populated_config
.add_source("test".to_string(), "https://github.com/test/repo.git".to_string());
assert!(!populated_config.sources.is_empty());
let populated_toml = toml::to_string_pretty(&populated_config).unwrap();
assert!(populated_toml.contains("[sources]"));
assert!(populated_toml.contains("test ="));
Ok(())
}
#[tokio::test]
async fn test_editor_fallback_logic() -> Result<()> {
if cfg!(target_os = "windows") {
let default = "notepad";
assert_eq!(default, "notepad");
} else {
let default = "vi";
assert_eq!(default, "vi");
}
Ok(())
}
#[tokio::test]
async fn test_config_save_and_load_cycle() -> Result<()> {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.toml");
let mut config = GlobalConfig::default();
config.add_source("github".to_string(), "https://github.com/test/repo.git".to_string());
config.add_source("gitlab".to_string(), "https://gitlab.com/test/repo.git".to_string());
config.add_source(
"private".to_string(),
"https://oauth2:token@github.com/org/repo.git".to_string(),
);
config.save_to(&config_path).await?;
assert!(config_path.exists());
let loaded = GlobalConfig::load_from(&config_path).await?;
assert_eq!(loaded.sources.len(), 3);
assert!(loaded.has_source("github"));
assert!(loaded.has_source("gitlab"));
assert!(loaded.has_source("private"));
assert_eq!(
loaded.get_source("github"),
Some(&"https://github.com/test/repo.git".to_string())
);
Ok(())
}
#[test]
fn test_config_subcommands_parsing() {
let init = ConfigSubcommands::Init {
force: true,
};
match init {
ConfigSubcommands::Init {
force,
} => assert!(force),
_ => panic!("Wrong variant"),
}
let add = ConfigSubcommands::AddSource {
name: "test".to_string(),
url: "https://github.com/test/repo.git".to_string(),
};
match add {
ConfigSubcommands::AddSource {
name,
url,
} => {
assert_eq!(name, "test");
assert!(url.contains("github"));
}
_ => panic!("Wrong variant"),
}
let remove = ConfigSubcommands::RemoveSource {
name: "test".to_string(),
};
match remove {
ConfigSubcommands::RemoveSource {
name,
} => assert_eq!(name, "test"),
_ => panic!("Wrong variant"),
}
}
}