🧪 test(init): unit tests for planning and file IO
This commit is contained in:
committed by
Jeremiah Russell
parent
0a047dd547
commit
2cfd16c8ac
@@ -417,13 +417,13 @@ impl InitCli {
|
|||||||
.with_prompt("Do you have a credential file to set up now?")
|
.with_prompt("Do you have a credential file to set up now?")
|
||||||
.default(true)
|
.default(true)
|
||||||
.interact()
|
.interact()
|
||||||
.map_err(|e| Error::FileIo(format!("Interactive prompt failed: {}", e)))?;
|
.map_err(|e| Error::FileIo(format!("Interactive prompt failed: {e}")))?;
|
||||||
|
|
||||||
if should_provide {
|
if should_provide {
|
||||||
let cred_path: String = Input::new()
|
let cred_path: String = Input::new()
|
||||||
.with_prompt("Path to credential JSON file")
|
.with_prompt("Path to credential JSON file")
|
||||||
.interact_text()
|
.interact_text()
|
||||||
.map_err(|e| Error::FileIo(format!("Interactive input failed: {}", e)))?;
|
.map_err(|e| Error::FileIo(format!("Interactive input failed: {e}")))?;
|
||||||
|
|
||||||
let cred_file = PathBuf::from(cred_path);
|
let cred_file = PathBuf::from(cred_path);
|
||||||
self.validate_credential_file(&cred_file)?;
|
self.validate_credential_file(&cred_file)?;
|
||||||
@@ -446,11 +446,11 @@ impl InitCli {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let content = fs::read_to_string(path)
|
let content = fs::read_to_string(path)
|
||||||
.map_err(|e| Error::FileIo(format!("Cannot read credential file: {}", e)))?;
|
.map_err(|e| Error::FileIo(format!("Cannot read credential file: {e}")))?;
|
||||||
|
|
||||||
// Try to parse as ConsoleApplicationSecret to validate format
|
// Try to parse as ConsoleApplicationSecret to validate format
|
||||||
serde_json::from_str::<ConsoleApplicationSecret>(&content).map_err(|e| {
|
serde_json::from_str::<ConsoleApplicationSecret>(&content).map_err(|e| {
|
||||||
Error::SerializationError(format!("Invalid credential file format: {}", e))
|
Error::SerializationError(format!("Invalid credential file format: {e}"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
log::info!("Credential file validated: {}", path.display());
|
log::info!("Credential file validated: {}", path.display());
|
||||||
@@ -588,7 +588,7 @@ impl InitCli {
|
|||||||
|
|
||||||
for (i, operation) in operations.iter().enumerate() {
|
for (i, operation) in operations.iter().enumerate() {
|
||||||
pb.set_position(i as u64);
|
pb.set_position(i as u64);
|
||||||
pb.set_message(format!("{}", operation));
|
pb.set_message(format!("{operation}"));
|
||||||
|
|
||||||
self.execute_operation(operation).await?;
|
self.execute_operation(operation).await?;
|
||||||
|
|
||||||
@@ -608,7 +608,7 @@ impl InitCli {
|
|||||||
Operation::CreateDir { path, .. } => {
|
Operation::CreateDir { path, .. } => {
|
||||||
log::info!("Creating directory: {}", path.display());
|
log::info!("Creating directory: {}", path.display());
|
||||||
fs::create_dir_all(path)
|
fs::create_dir_all(path)
|
||||||
.map_err(|e| Error::FileIo(format!("Failed to create directory: {}", e)))?;
|
.map_err(|e| Error::FileIo(format!("Failed to create directory: {e}")))?;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
if let Some(mode) = operation.get_mode() {
|
if let Some(mode) = operation.get_mode() {
|
||||||
@@ -626,10 +626,10 @@ impl InitCli {
|
|||||||
self.create_backup(to)?;
|
self.create_backup(to)?;
|
||||||
} else if to.exists() && self.interactive {
|
} else if to.exists() && self.interactive {
|
||||||
let should_overwrite = Confirm::new()
|
let should_overwrite = Confirm::new()
|
||||||
.with_prompt(&format!("Overwrite existing file {}?", to.display()))
|
.with_prompt(format!("Overwrite existing file {}?", to.display()))
|
||||||
.default(false)
|
.default(false)
|
||||||
.interact()
|
.interact()
|
||||||
.map_err(|e| Error::FileIo(format!("Interactive prompt failed: {}", e)))?;
|
.map_err(|e| Error::FileIo(format!("Interactive prompt failed: {e}")))?;
|
||||||
|
|
||||||
if !should_overwrite {
|
if !should_overwrite {
|
||||||
log::info!("Skipping file copy due to user choice");
|
log::info!("Skipping file copy due to user choice");
|
||||||
@@ -641,7 +641,7 @@ impl InitCli {
|
|||||||
|
|
||||||
log::info!("Copying file: {} → {}", from.display(), to.display());
|
log::info!("Copying file: {} → {}", from.display(), to.display());
|
||||||
fs::copy(from, to)
|
fs::copy(from, to)
|
||||||
.map_err(|e| Error::FileIo(format!("Failed to copy file: {}", e)))?;
|
.map_err(|e| Error::FileIo(format!("Failed to copy file: {e}")))?;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
if let Some(mode) = operation.get_mode() {
|
if let Some(mode) = operation.get_mode() {
|
||||||
@@ -659,10 +659,10 @@ impl InitCli {
|
|||||||
self.create_backup(path)?;
|
self.create_backup(path)?;
|
||||||
} else if path.exists() && self.interactive {
|
} else if path.exists() && self.interactive {
|
||||||
let should_overwrite = Confirm::new()
|
let should_overwrite = Confirm::new()
|
||||||
.with_prompt(&format!("Overwrite existing file {}?", path.display()))
|
.with_prompt(format!("Overwrite existing file {}?", path.display()))
|
||||||
.default(false)
|
.default(false)
|
||||||
.interact()
|
.interact()
|
||||||
.map_err(|e| Error::FileIo(format!("Interactive prompt failed: {}", e)))?;
|
.map_err(|e| Error::FileIo(format!("Interactive prompt failed: {e}")))?;
|
||||||
|
|
||||||
if !should_overwrite {
|
if !should_overwrite {
|
||||||
log::info!("Skipping file write due to user choice");
|
log::info!("Skipping file write due to user choice");
|
||||||
@@ -674,7 +674,7 @@ impl InitCli {
|
|||||||
|
|
||||||
log::info!("Writing file: {}", path.display());
|
log::info!("Writing file: {}", path.display());
|
||||||
fs::write(path, contents)
|
fs::write(path, contents)
|
||||||
.map_err(|e| Error::FileIo(format!("Failed to write file: {}", e)))?;
|
.map_err(|e| Error::FileIo(format!("Failed to write file: {e}")))?;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
if let Some(mode) = operation.get_mode() {
|
if let Some(mode) = operation.get_mode() {
|
||||||
@@ -685,7 +685,7 @@ impl InitCli {
|
|||||||
Operation::EnsureTokenDir { path, .. } => {
|
Operation::EnsureTokenDir { path, .. } => {
|
||||||
log::info!("Ensuring token directory: {}", path.display());
|
log::info!("Ensuring token directory: {}", path.display());
|
||||||
fs::create_dir_all(path).map_err(|e| {
|
fs::create_dir_all(path).map_err(|e| {
|
||||||
Error::FileIo(format!("Failed to create token directory: {}", e))
|
Error::FileIo(format!("Failed to create token directory: {e}"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
@@ -713,7 +713,7 @@ impl InitCli {
|
|||||||
/// Create a timestamped backup of a file.
|
/// Create a timestamped backup of a file.
|
||||||
fn create_backup(&self, file_path: &Path) -> Result<()> {
|
fn create_backup(&self, file_path: &Path) -> Result<()> {
|
||||||
let timestamp = Local::now().format("%Y%m%d%H%M%S");
|
let timestamp = Local::now().format("%Y%m%d%H%M%S");
|
||||||
let backup_path = file_path.with_extension(format!("bak-{}", timestamp));
|
let backup_path = file_path.with_extension(format!("bak-{timestamp}"));
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Creating backup: {} → {}",
|
"Creating backup: {} → {}",
|
||||||
@@ -721,7 +721,7 @@ impl InitCli {
|
|||||||
backup_path.display()
|
backup_path.display()
|
||||||
);
|
);
|
||||||
fs::copy(file_path, &backup_path)
|
fs::copy(file_path, &backup_path)
|
||||||
.map_err(|e| Error::FileIo(format!("Failed to create backup: {}", e)))?;
|
.map_err(|e| Error::FileIo(format!("Failed to create backup: {e}")))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -732,13 +732,13 @@ impl InitCli {
|
|||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
let metadata = fs::metadata(path)
|
let metadata = fs::metadata(path)
|
||||||
.map_err(|e| Error::FileIo(format!("Failed to get file metadata: {}", e)))?;
|
.map_err(|e| Error::FileIo(format!("Failed to get file metadata: {e}")))?;
|
||||||
|
|
||||||
let mut permissions = metadata.permissions();
|
let mut permissions = metadata.permissions();
|
||||||
permissions.set_mode(mode);
|
permissions.set_mode(mode);
|
||||||
|
|
||||||
fs::set_permissions(path, permissions)
|
fs::set_permissions(path, permissions)
|
||||||
.map_err(|e| Error::FileIo(format!("Failed to set file permissions: {}", e)))?;
|
.map_err(|e| Error::FileIo(format!("Failed to set file permissions: {e}")))?;
|
||||||
|
|
||||||
log::debug!("Set permissions {:o} on {}", mode, path.display());
|
log::debug!("Set permissions {:o} on {}", mode, path.display());
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -760,7 +760,7 @@ impl InitCli {
|
|||||||
// Initialize Gmail client which will trigger OAuth flow if needed
|
// Initialize Gmail client which will trigger OAuth flow if needed
|
||||||
let client = GmailClient::new_with_config(client_config)
|
let client = GmailClient::new_with_config(client_config)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::FileIo(format!("OAuth2 authentication failed: {}", e)))?;
|
.map_err(|e| Error::FileIo(format!("OAuth2 authentication failed: {e}")))?;
|
||||||
|
|
||||||
// The client initialization already verified the connection by fetching labels
|
// The client initialization already verified the connection by fetching labels
|
||||||
// We can just show some labels to confirm it's working
|
// We can just show some labels to confirm it's working
|
||||||
@@ -821,3 +821,6 @@ impl Operation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|||||||
372
src/cli/init_cli/tests.rs
Normal file
372
src/cli/init_cli/tests.rs
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
//! Unit tests for init CLI functionality.
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod unit_tests {
|
||||||
|
use super::super::*;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Test helper to create a mock credential file
|
||||||
|
fn create_mock_credential_file(dir: &Path) -> std::io::Result<()> {
|
||||||
|
let credential_content = r#"{
|
||||||
|
"installed": {
|
||||||
|
"client_id": "test-client-id.googleusercontent.com",
|
||||||
|
"client_secret": "test-client-secret",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"redirect_uris": ["http://localhost"]
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
fs::write(dir.join("credential.json"), credential_content)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_config_root_home() {
|
||||||
|
let result = parse_config_root("h:.test-config");
|
||||||
|
let home = env::home_dir().unwrap_or_default();
|
||||||
|
assert_eq!(result, home.join(".test-config"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_config_root_current() {
|
||||||
|
let result = parse_config_root("c:.test-config");
|
||||||
|
let current = env::current_dir().unwrap_or_default();
|
||||||
|
assert_eq!(result, current.join(".test-config"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_config_root_root() {
|
||||||
|
let result = parse_config_root("r:etc/cull-gmail");
|
||||||
|
assert_eq!(result, std::path::PathBuf::from("/etc/cull-gmail"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_config_root_no_prefix() {
|
||||||
|
let result = parse_config_root("/absolute/path");
|
||||||
|
assert_eq!(result, std::path::PathBuf::from("/absolute/path"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_defaults() {
|
||||||
|
assert_eq!(InitDefaults::credential_filename(), "credential.json");
|
||||||
|
assert_eq!(InitDefaults::config_filename(), "cull-gmail.toml");
|
||||||
|
assert_eq!(InitDefaults::rules_filename(), "rules.toml");
|
||||||
|
assert_eq!(InitDefaults::token_dir_name(), "gmail1");
|
||||||
|
|
||||||
|
// Test that config content contains expected keys
|
||||||
|
let config_content = InitDefaults::CONFIG_FILE_CONTENT;
|
||||||
|
assert!(config_content.contains("credential_file = \"credential.json\""));
|
||||||
|
assert!(config_content.contains("config_root = \"h:.cull-gmail\""));
|
||||||
|
assert!(config_content.contains("execute = false"));
|
||||||
|
|
||||||
|
// Test that rules content is a valid template
|
||||||
|
let rules_content = InitDefaults::RULES_FILE_CONTENT;
|
||||||
|
assert!(rules_content.contains("# Example rules for cull-gmail"));
|
||||||
|
assert!(rules_content.contains("older_than:30d"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_credential_file_success() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
create_mock_credential_file(temp_dir.path()).unwrap();
|
||||||
|
|
||||||
|
let init_cli = InitCli {
|
||||||
|
config_dir: "test".to_string(),
|
||||||
|
credential_file: None,
|
||||||
|
force: false,
|
||||||
|
dry_run: false,
|
||||||
|
interactive: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let credential_path = temp_dir.path().join("credential.json");
|
||||||
|
let result = init_cli.validate_credential_file(&credential_path);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_credential_file_not_found() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let init_cli = InitCli {
|
||||||
|
config_dir: "test".to_string(),
|
||||||
|
credential_file: None,
|
||||||
|
force: false,
|
||||||
|
dry_run: false,
|
||||||
|
interactive: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let nonexistent_path = temp_dir.path().join("nonexistent.json");
|
||||||
|
let result = init_cli.validate_credential_file(&nonexistent_path);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().to_string().contains("not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_credential_file_invalid_json() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let credential_path = temp_dir.path().join("invalid.json");
|
||||||
|
fs::write(&credential_path, "invalid json content").unwrap();
|
||||||
|
|
||||||
|
let init_cli = InitCli {
|
||||||
|
config_dir: "test".to_string(),
|
||||||
|
credential_file: None,
|
||||||
|
force: false,
|
||||||
|
dry_run: false,
|
||||||
|
interactive: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = init_cli.validate_credential_file(&credential_path);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().to_string().contains("Invalid credential file format"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_plan_operations_new_setup() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("new-config");
|
||||||
|
|
||||||
|
let init_cli = InitCli {
|
||||||
|
config_dir: "test".to_string(),
|
||||||
|
credential_file: None,
|
||||||
|
force: false,
|
||||||
|
dry_run: false,
|
||||||
|
interactive: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let operations = init_cli.plan_operations(&config_path, None).unwrap();
|
||||||
|
|
||||||
|
// Should have: CreateDir, WriteFile (config), WriteFile (rules), EnsureTokenDir
|
||||||
|
assert_eq!(operations.len(), 4);
|
||||||
|
|
||||||
|
match &operations[0] {
|
||||||
|
Operation::CreateDir { path, .. } => {
|
||||||
|
assert_eq!(path, &config_path);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected CreateDir operation"),
|
||||||
|
}
|
||||||
|
|
||||||
|
match &operations[1] {
|
||||||
|
Operation::WriteFile { path, contents, .. } => {
|
||||||
|
assert_eq!(path, &config_path.join("cull-gmail.toml"));
|
||||||
|
assert!(contents.contains("credential_file = \"credential.json\""));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected WriteFile operation for config"),
|
||||||
|
}
|
||||||
|
|
||||||
|
match &operations[2] {
|
||||||
|
Operation::WriteFile { path, contents, .. } => {
|
||||||
|
assert_eq!(path, &config_path.join("rules.toml"));
|
||||||
|
assert!(contents.contains("# Example rules for cull-gmail"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected WriteFile operation for rules"),
|
||||||
|
}
|
||||||
|
|
||||||
|
match &operations[3] {
|
||||||
|
Operation::EnsureTokenDir { path, .. } => {
|
||||||
|
assert_eq!(path, &config_path.join("gmail1"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected EnsureTokenDir operation"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_plan_operations_with_credential_file() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("new-config");
|
||||||
|
let cred_path = temp_dir.path().join("cred.json");
|
||||||
|
create_mock_credential_file(temp_dir.path()).unwrap();
|
||||||
|
fs::rename(temp_dir.path().join("credential.json"), &cred_path).unwrap();
|
||||||
|
|
||||||
|
let init_cli = InitCli {
|
||||||
|
config_dir: "test".to_string(),
|
||||||
|
credential_file: None,
|
||||||
|
force: false,
|
||||||
|
dry_run: false,
|
||||||
|
interactive: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let operations = init_cli.plan_operations(&config_path, Some(&cred_path)).unwrap();
|
||||||
|
|
||||||
|
// Should have: CreateDir, CopyFile (credential), WriteFile (config), WriteFile (rules), EnsureTokenDir, RunOAuth2
|
||||||
|
assert_eq!(operations.len(), 6);
|
||||||
|
|
||||||
|
// Check that CopyFile operation exists
|
||||||
|
let copy_op = operations.iter().find(|op| matches!(op, Operation::CopyFile { .. }));
|
||||||
|
assert!(copy_op.is_some());
|
||||||
|
|
||||||
|
// Check that RunOAuth2 operation exists
|
||||||
|
let oauth_op = operations.iter().find(|op| matches!(op, Operation::RunOAuth2 { .. }));
|
||||||
|
assert!(oauth_op.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_plan_operations_existing_config_no_force() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("existing-config");
|
||||||
|
fs::create_dir_all(&config_path).unwrap();
|
||||||
|
|
||||||
|
// Create existing config file
|
||||||
|
fs::write(config_path.join("cull-gmail.toml"), "existing config").unwrap();
|
||||||
|
|
||||||
|
let init_cli = InitCli {
|
||||||
|
config_dir: "test".to_string(),
|
||||||
|
credential_file: None,
|
||||||
|
force: false,
|
||||||
|
dry_run: false,
|
||||||
|
interactive: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = init_cli.plan_operations(&config_path, None);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().to_string().contains("already exists"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_plan_operations_existing_config_with_force() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = temp_dir.path().join("existing-config");
|
||||||
|
fs::create_dir_all(&config_path).unwrap();
|
||||||
|
|
||||||
|
// Create existing config file
|
||||||
|
fs::write(config_path.join("cull-gmail.toml"), "existing config").unwrap();
|
||||||
|
fs::write(config_path.join("rules.toml"), "existing rules").unwrap();
|
||||||
|
|
||||||
|
let init_cli = InitCli {
|
||||||
|
config_dir: "test".to_string(),
|
||||||
|
credential_file: None,
|
||||||
|
force: true,
|
||||||
|
dry_run: false,
|
||||||
|
interactive: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let operations = init_cli.plan_operations(&config_path, None).unwrap();
|
||||||
|
|
||||||
|
// Should succeed and plan backup operations
|
||||||
|
let config_op = operations.iter().find(|op| {
|
||||||
|
if let Operation::WriteFile { path, backup_if_exists, .. } = op {
|
||||||
|
path.file_name().unwrap() == "cull-gmail.toml" && *backup_if_exists
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert!(config_op.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_backup() {
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let test_file = temp_dir.path().join("test.txt");
|
||||||
|
fs::write(&test_file, "test content").unwrap();
|
||||||
|
|
||||||
|
let init_cli = InitCli {
|
||||||
|
config_dir: "test".to_string(),
|
||||||
|
credential_file: None,
|
||||||
|
force: false,
|
||||||
|
dry_run: false,
|
||||||
|
interactive: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = init_cli.create_backup(&test_file);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
// Check that a backup file was created
|
||||||
|
let backup_files: Vec<_> = fs::read_dir(temp_dir.path())
|
||||||
|
.unwrap()
|
||||||
|
.filter_map(|entry| {
|
||||||
|
let entry = entry.ok()?;
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if name.starts_with("test.bak-") {
|
||||||
|
Some(name)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert_eq!(backup_files.len(), 1);
|
||||||
|
|
||||||
|
// Verify backup content
|
||||||
|
let backup_path = temp_dir.path().join(&backup_files[0]);
|
||||||
|
let backup_content = fs::read_to_string(backup_path).unwrap();
|
||||||
|
assert_eq!(backup_content, "test content");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn test_set_permissions() {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
let temp_dir = TempDir::new().unwrap();
|
||||||
|
let test_file = temp_dir.path().join("test.txt");
|
||||||
|
fs::write(&test_file, "test content").unwrap();
|
||||||
|
|
||||||
|
let init_cli = InitCli {
|
||||||
|
config_dir: "test".to_string(),
|
||||||
|
credential_file: None,
|
||||||
|
force: false,
|
||||||
|
dry_run: false,
|
||||||
|
interactive: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = init_cli.set_permissions(&test_file, 0o600);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let metadata = fs::metadata(&test_file).unwrap();
|
||||||
|
let permissions = metadata.permissions();
|
||||||
|
assert_eq!(permissions.mode() & 0o777, 0o600);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_operation_display() {
|
||||||
|
let temp_path = std::path::PathBuf::from("/tmp/test");
|
||||||
|
|
||||||
|
let create_dir_op = Operation::CreateDir {
|
||||||
|
path: temp_path.clone(),
|
||||||
|
#[cfg(unix)]
|
||||||
|
mode: Some(0o755),
|
||||||
|
};
|
||||||
|
assert_eq!(format!("{create_dir_op}"), "Create directory: /tmp/test");
|
||||||
|
|
||||||
|
let copy_file_op = Operation::CopyFile {
|
||||||
|
from: temp_path.clone(),
|
||||||
|
to: temp_path.join("dest"),
|
||||||
|
#[cfg(unix)]
|
||||||
|
mode: Some(0o600),
|
||||||
|
backup_if_exists: false,
|
||||||
|
};
|
||||||
|
assert_eq!(format!("{copy_file_op}"), "Copy file: /tmp/test → /tmp/test/dest");
|
||||||
|
|
||||||
|
let write_file_op = Operation::WriteFile {
|
||||||
|
path: temp_path.clone(),
|
||||||
|
contents: "content".to_string(),
|
||||||
|
#[cfg(unix)]
|
||||||
|
mode: Some(0o644),
|
||||||
|
backup_if_exists: false,
|
||||||
|
};
|
||||||
|
assert_eq!(format!("{write_file_op}"), "Write file: /tmp/test");
|
||||||
|
|
||||||
|
let oauth_op = Operation::RunOAuth2 {
|
||||||
|
config_root: "h:.config".to_string(),
|
||||||
|
credential_file: Some("cred.json".to_string()),
|
||||||
|
};
|
||||||
|
assert_eq!(format!("{oauth_op}"), "Run OAuth2 authentication flow");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn test_operation_get_mode() {
|
||||||
|
let temp_path = std::path::PathBuf::from("/tmp/test");
|
||||||
|
|
||||||
|
let create_dir_op = Operation::CreateDir {
|
||||||
|
path: temp_path.clone(),
|
||||||
|
mode: Some(0o755),
|
||||||
|
};
|
||||||
|
assert_eq!(create_dir_op.get_mode(), Some(0o755));
|
||||||
|
|
||||||
|
let oauth_op = Operation::RunOAuth2 {
|
||||||
|
config_root: "h:.config".to_string(),
|
||||||
|
credential_file: Some("cred.json".to_string()),
|
||||||
|
};
|
||||||
|
assert_eq!(oauth_op.get_mode(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
319
tests/init_integration_tests.rs
Normal file
319
tests/init_integration_tests.rs
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
//! Integration tests for the init CLI command.
|
||||||
|
|
||||||
|
use assert_cmd::prelude::*;
|
||||||
|
use assert_fs::prelude::*;
|
||||||
|
use predicates::prelude::*;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_help() {
|
||||||
|
let mut cmd = Command::cargo_bin("cull-gmail").unwrap();
|
||||||
|
cmd.args(["init", "--help"]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("Initialize cull-gmail configuration"))
|
||||||
|
.stdout(predicate::str::contains("--config-dir"))
|
||||||
|
.stdout(predicate::str::contains("--credential-file"))
|
||||||
|
.stdout(predicate::str::contains("--dry-run"))
|
||||||
|
.stdout(predicate::str::contains("--interactive"))
|
||||||
|
.stdout(predicate::str::contains("--force"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_dry_run_new_setup() {
|
||||||
|
let temp_dir = assert_fs::TempDir::new().unwrap();
|
||||||
|
let config_dir = temp_dir.path().join("test-config");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("cull-gmail").unwrap();
|
||||||
|
cmd.args([
|
||||||
|
"init",
|
||||||
|
"--config-dir",
|
||||||
|
&format!("c:{}", config_dir.to_string_lossy()),
|
||||||
|
"--dry-run"
|
||||||
|
]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("DRY RUN: No changes will be made"))
|
||||||
|
.stdout(predicate::str::contains("Planned operations:"))
|
||||||
|
.stdout(predicate::str::contains("Create directory:"))
|
||||||
|
.stdout(predicate::str::contains("Write file:"))
|
||||||
|
.stdout(predicate::str::contains("cull-gmail.toml"))
|
||||||
|
.stdout(predicate::str::contains("rules.toml"))
|
||||||
|
.stdout(predicate::str::contains("Ensure token directory:"))
|
||||||
|
.stdout(predicate::str::contains("gmail1"))
|
||||||
|
.stdout(predicate::str::contains("OAuth2 authentication skipped"))
|
||||||
|
.stdout(predicate::str::contains("To apply these changes, run without --dry-run"));
|
||||||
|
|
||||||
|
// Verify no files were actually created
|
||||||
|
assert!(!config_dir.exists());
|
||||||
|
|
||||||
|
temp_dir.close().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_dry_run_with_credential_file() {
|
||||||
|
let temp_dir = assert_fs::TempDir::new().unwrap();
|
||||||
|
let config_dir = temp_dir.path().join("test-config");
|
||||||
|
let credential_file = temp_dir.child("credential.json");
|
||||||
|
|
||||||
|
// Create a mock credential file
|
||||||
|
credential_file.write_str(r#"{
|
||||||
|
"installed": {
|
||||||
|
"client_id": "test-client-id.googleusercontent.com",
|
||||||
|
"client_secret": "test-client-secret",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"redirect_uris": ["http://localhost"]
|
||||||
|
}
|
||||||
|
}"#).unwrap();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("cull-gmail").unwrap();
|
||||||
|
cmd.args([
|
||||||
|
"init",
|
||||||
|
"--config-dir",
|
||||||
|
&format!("c:{}", config_dir.to_string_lossy()),
|
||||||
|
"--credential-file",
|
||||||
|
credential_file.path().to_str().unwrap(),
|
||||||
|
"--dry-run"
|
||||||
|
]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("DRY RUN: No changes will be made"))
|
||||||
|
.stdout(predicate::str::contains("Planned operations:"))
|
||||||
|
.stdout(predicate::str::contains("Copy file:"))
|
||||||
|
.stdout(predicate::str::contains("credential.json"))
|
||||||
|
.stdout(predicate::str::contains("OAuth2 authentication would open"));
|
||||||
|
|
||||||
|
// Verify no files were actually created
|
||||||
|
assert!(!config_dir.exists());
|
||||||
|
|
||||||
|
temp_dir.close().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_actual_execution() {
|
||||||
|
let temp_dir = assert_fs::TempDir::new().unwrap();
|
||||||
|
let config_dir = temp_dir.path().join("test-config");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("cull-gmail").unwrap();
|
||||||
|
cmd.args([
|
||||||
|
"init",
|
||||||
|
"--config-dir",
|
||||||
|
&format!("c:{}", config_dir.to_string_lossy()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("Initialization completed successfully!"))
|
||||||
|
.stdout(predicate::str::contains("Configuration directory:"))
|
||||||
|
.stdout(predicate::str::contains("Files created:"))
|
||||||
|
.stdout(predicate::str::contains("cull-gmail.toml"))
|
||||||
|
.stdout(predicate::str::contains("rules.toml"))
|
||||||
|
.stdout(predicate::str::contains("Next steps:"));
|
||||||
|
|
||||||
|
// Verify files were actually created
|
||||||
|
assert!(config_dir.exists());
|
||||||
|
assert!(config_dir.join("cull-gmail.toml").exists());
|
||||||
|
assert!(config_dir.join("rules.toml").exists());
|
||||||
|
assert!(config_dir.join("gmail1").exists());
|
||||||
|
|
||||||
|
// Verify file contents
|
||||||
|
let config_content = std::fs::read_to_string(config_dir.join("cull-gmail.toml")).unwrap();
|
||||||
|
assert!(config_content.contains("credential_file = \"credential.json\""));
|
||||||
|
assert!(config_content.contains("execute = false"));
|
||||||
|
|
||||||
|
let rules_content = std::fs::read_to_string(config_dir.join("rules.toml")).unwrap();
|
||||||
|
assert!(rules_content.contains("# Example rules for cull-gmail"));
|
||||||
|
|
||||||
|
temp_dir.close().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_force_overwrite() {
|
||||||
|
let temp_dir = assert_fs::TempDir::new().unwrap();
|
||||||
|
let config_dir = temp_dir.path().join("test-config");
|
||||||
|
|
||||||
|
// Create config directory and file first
|
||||||
|
std::fs::create_dir_all(&config_dir).unwrap();
|
||||||
|
std::fs::write(config_dir.join("cull-gmail.toml"), "old config").unwrap();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("cull-gmail").unwrap();
|
||||||
|
cmd.args([
|
||||||
|
"init",
|
||||||
|
"--config-dir",
|
||||||
|
&format!("c:{}", config_dir.to_string_lossy()),
|
||||||
|
"--force",
|
||||||
|
"--dry-run"
|
||||||
|
]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("DRY RUN: No changes will be made"))
|
||||||
|
.stdout(predicate::str::contains("(with backup)"));
|
||||||
|
|
||||||
|
temp_dir.close().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_existing_config_no_force_fails() {
|
||||||
|
let temp_dir = assert_fs::TempDir::new().unwrap();
|
||||||
|
let config_dir = temp_dir.path().join("test-config");
|
||||||
|
|
||||||
|
// Create config directory and file first
|
||||||
|
std::fs::create_dir_all(&config_dir).unwrap();
|
||||||
|
std::fs::write(config_dir.join("cull-gmail.toml"), "existing config").unwrap();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("cull-gmail").unwrap();
|
||||||
|
cmd.args([
|
||||||
|
"init",
|
||||||
|
"--config-dir",
|
||||||
|
&format!("c:{}", config_dir.to_string_lossy()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::contains("already exists"))
|
||||||
|
.stderr(predicate::str::contains("--force"))
|
||||||
|
.stderr(predicate::str::contains("--interactive"));
|
||||||
|
|
||||||
|
temp_dir.close().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_with_credential_file_copy() {
|
||||||
|
let temp_dir = assert_fs::TempDir::new().unwrap();
|
||||||
|
let config_dir = temp_dir.path().join("test-config");
|
||||||
|
let credential_file = temp_dir.child("source_credential.json");
|
||||||
|
|
||||||
|
// Create a mock credential file
|
||||||
|
credential_file.write_str(r#"{
|
||||||
|
"installed": {
|
||||||
|
"client_id": "test-client-id.googleusercontent.com",
|
||||||
|
"client_secret": "test-client-secret",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"redirect_uris": ["http://localhost"]
|
||||||
|
}
|
||||||
|
}"#).unwrap();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("cull-gmail").unwrap();
|
||||||
|
cmd.args([
|
||||||
|
"init",
|
||||||
|
"--config-dir",
|
||||||
|
&format!("c:{}", config_dir.to_string_lossy()),
|
||||||
|
"--credential-file",
|
||||||
|
credential_file.path().to_str().unwrap(),
|
||||||
|
// Skip OAuth for this test since we don't have real credentials
|
||||||
|
]);
|
||||||
|
|
||||||
|
// This will fail at OAuth step, but we can check that files were created correctly
|
||||||
|
let _output = cmd.output().unwrap();
|
||||||
|
|
||||||
|
// Verify files were created up to the OAuth step
|
||||||
|
assert!(config_dir.exists());
|
||||||
|
assert!(config_dir.join("cull-gmail.toml").exists());
|
||||||
|
assert!(config_dir.join("rules.toml").exists());
|
||||||
|
assert!(config_dir.join("credential.json").exists());
|
||||||
|
assert!(config_dir.join("gmail1").exists());
|
||||||
|
|
||||||
|
// Verify credential file was copied
|
||||||
|
let copied_credential_content = std::fs::read_to_string(config_dir.join("credential.json")).unwrap();
|
||||||
|
assert!(copied_credential_content.contains("test-client-id.googleusercontent.com"));
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
// Verify credential file has secure permissions
|
||||||
|
let metadata = std::fs::metadata(config_dir.join("credential.json")).unwrap();
|
||||||
|
let permissions = metadata.permissions();
|
||||||
|
assert_eq!(permissions.mode() & 0o777, 0o600);
|
||||||
|
}
|
||||||
|
|
||||||
|
temp_dir.close().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_invalid_credential_file() {
|
||||||
|
let temp_dir = assert_fs::TempDir::new().unwrap();
|
||||||
|
let config_dir = temp_dir.path().join("test-config");
|
||||||
|
let credential_file = temp_dir.child("invalid_credential.json");
|
||||||
|
|
||||||
|
// Create an invalid credential file
|
||||||
|
credential_file.write_str("invalid json content").unwrap();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("cull-gmail").unwrap();
|
||||||
|
cmd.args([
|
||||||
|
"init",
|
||||||
|
"--config-dir",
|
||||||
|
&format!("c:{}", config_dir.to_string_lossy()),
|
||||||
|
"--credential-file",
|
||||||
|
credential_file.path().to_str().unwrap(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::contains("Invalid credential file format"));
|
||||||
|
|
||||||
|
temp_dir.close().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_nonexistent_credential_file() {
|
||||||
|
let temp_dir = assert_fs::TempDir::new().unwrap();
|
||||||
|
let config_dir = temp_dir.path().join("test-config");
|
||||||
|
let nonexistent_file = temp_dir.path().join("nonexistent.json");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("cull-gmail").unwrap();
|
||||||
|
cmd.args([
|
||||||
|
"init",
|
||||||
|
"--config-dir",
|
||||||
|
&format!("c:{}", config_dir.to_string_lossy()),
|
||||||
|
"--credential-file",
|
||||||
|
nonexistent_file.to_str().unwrap(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.failure()
|
||||||
|
.stderr(predicate::str::contains("not found"));
|
||||||
|
|
||||||
|
temp_dir.close().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test would require real Gmail credentials and should be ignored by default
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires real Gmail OAuth2 credentials"]
|
||||||
|
fn test_init_oauth_integration() {
|
||||||
|
// This test should only run when CULL_GMAIL_TEST_CREDENTIAL_FILE is set
|
||||||
|
let credential_file = std::env::var("CULL_GMAIL_TEST_CREDENTIAL_FILE")
|
||||||
|
.expect("CULL_GMAIL_TEST_CREDENTIAL_FILE must be set for OAuth integration test");
|
||||||
|
|
||||||
|
let temp_dir = assert_fs::TempDir::new().unwrap();
|
||||||
|
let config_dir = temp_dir.path().join("test-config");
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("cull-gmail").unwrap();
|
||||||
|
cmd.args([
|
||||||
|
"init",
|
||||||
|
"--config-dir",
|
||||||
|
&format!("c:{}", config_dir.to_string_lossy()),
|
||||||
|
"--credential-file",
|
||||||
|
&credential_file,
|
||||||
|
]);
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("OAuth2 authentication successful!"))
|
||||||
|
.stdout(predicate::str::contains("gmail1/ (OAuth2 token cache)"));
|
||||||
|
|
||||||
|
// Verify token files were created
|
||||||
|
assert!(config_dir.join("gmail1").exists());
|
||||||
|
|
||||||
|
// Check if there are token-related files in the gmail1 directory
|
||||||
|
let gmail_dir_contents = std::fs::read_dir(config_dir.join("gmail1")).unwrap();
|
||||||
|
let has_token_files = gmail_dir_contents.count() > 0;
|
||||||
|
assert!(has_token_files, "Expected token files to be created in gmail1 directory");
|
||||||
|
|
||||||
|
temp_dir.close().unwrap();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user