diff --git a/src/bin/gman/cli.rs b/src/bin/gman/cli.rs index 649bf62..e3a8f23 100644 --- a/src/bin/gman/cli.rs +++ b/src/bin/gman/cli.rs @@ -1,7 +1,8 @@ use crate::command::preview_command; use anyhow::{Context, Result, anyhow}; +use clap_complete::CompletionCandidate; use futures::future::join_all; -use gman::config::{load_config, Config, RunConfig}; +use gman::config::{Config, RunConfig, load_config}; use log::{debug, error}; use regex::Regex; use std::collections::HashMap; @@ -9,7 +10,6 @@ use std::ffi::{OsStr, OsString}; use std::fs; use std::path::PathBuf; use std::process::Command; -use clap_complete::CompletionCandidate; use tokio::runtime::Handle; const ARG_FORMAT_PLACEHOLDER_KEY: &str = "{{key}}"; @@ -256,48 +256,49 @@ pub fn parse_args( } pub fn run_config_completer(current: &OsStr) -> Vec { - let cur = current.to_string_lossy(); - match load_config() { - Ok(config) => { - if let Some(run_configs) = config.run_configs { - run_configs - .iter() - .filter(|rc| { - rc.name - .as_ref() - .expect("run config has no name") - .starts_with(&*cur) - }) - .map(|rc| { - CompletionCandidate::new(rc.name.as_ref().expect("run config has no name")) - }) - .collect() - } else { - vec![] - } - } - Err(_) => vec![], - } + let cur = current.to_string_lossy(); + match load_config() { + Ok(config) => { + if let Some(run_configs) = config.run_configs { + run_configs + .iter() + .filter(|rc| { + rc.name + .as_ref() + .expect("run config has no name") + .starts_with(&*cur) + }) + .map(|rc| { + CompletionCandidate::new(rc.name.as_ref().expect("run config has no name")) + }) + .collect() + } else { + vec![] + } + } + Err(_) => vec![], + } } pub fn secrets_completer(current: &OsStr) -> Vec { - let cur = current.to_string_lossy(); - match load_config() { - Ok(config) => { - let mut provider_config = match config.extract_provider_config(None) { - Ok(pc) => pc, - Err(_) => return vec![], - }; - let secrets_provider = provider_config.extract_provider(); - let h = Handle::current(); - tokio::task::block_in_place(|| h.block_on(secrets_provider.list_secrets())).unwrap_or_default() - .into_iter() - .filter(|s| s.starts_with(&*cur)) - .map(CompletionCandidate::new) - .collect() - } - Err(_) => vec![], - } + let cur = current.to_string_lossy(); + match load_config() { + Ok(config) => { + let mut provider_config = match config.extract_provider_config(None) { + Ok(pc) => pc, + Err(_) => return vec![], + }; + let secrets_provider = provider_config.extract_provider(); + let h = Handle::current(); + tokio::task::block_in_place(|| h.block_on(secrets_provider.list_secrets())) + .unwrap_or_default() + .into_iter() + .filter(|s| s.starts_with(&*cur)) + .map(CompletionCandidate::new) + .collect() + } + Err(_) => vec![], + } } #[cfg(test)] @@ -402,11 +403,10 @@ mod tests { ..Config::default() }; - // Capture stderr for dry_run preview let tokens = vec![OsString::from("echo"), OsString::from("hello")]; - // Best-effort: ensure function does not error under dry_run - let res = wrap_and_run_command(None, &cfg, tokens, None, true).await; - assert!(res.is_ok()); - // Not asserting output text to keep test platform-agnostic + let err = wrap_and_run_command(None, &cfg, tokens, None, true) + .await + .expect_err("expected failed secret resolution in dry_run"); + assert!(err.to_string().contains("Failed to fetch")); } } diff --git a/src/bin/gman/main.rs b/src/bin/gman/main.rs index e8779b6..20e4587 100644 --- a/src/bin/gman/main.rs +++ b/src/bin/gman/main.rs @@ -3,12 +3,12 @@ use crate::cli::secrets_completer; use anyhow::{Context, Result}; use clap::Subcommand; use clap::{ - crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser, ValueEnum, + CommandFactory, Parser, ValueEnum, crate_authors, crate_description, crate_name, crate_version, }; use clap_complete::{ArgValueCompleter, CompleteEnv}; use crossterm::execute; -use crossterm::terminal::{disable_raw_mode, LeaveAlternateScreen}; -use gman::config::{get_config_file_path, load_config, Config}; +use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode}; +use gman::config::{Config, get_config_file_path, load_config}; use std::ffi::OsString; use std::io::{self, IsTerminal, Read, Write}; use std::panic::PanicHookInfo; @@ -87,7 +87,7 @@ enum Commands { #[clap(alias = "show")] Get { /// Name of the secret to retrieve - #[arg(add = ArgValueCompleter::new(secrets_completer))] + #[arg(add = ArgValueCompleter::new(secrets_completer))] name: String, }, @@ -95,7 +95,7 @@ enum Commands { /// If a provider does not support updating secrets, this command will return an error. Update { /// Name of the secret to update - #[arg(add = ArgValueCompleter::new(secrets_completer))] + #[arg(add = ArgValueCompleter::new(secrets_completer))] name: String, }, @@ -103,7 +103,7 @@ enum Commands { #[clap(aliases = &["remove", "rm"])] Delete { /// Name of the secret to delete - #[arg(add = ArgValueCompleter::new(secrets_completer))] + #[arg(add = ArgValueCompleter::new(secrets_completer))] name: String, }, diff --git a/src/config.rs b/src/config.rs index 21c43ad..d3ca4e9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,6 +11,7 @@ //! //! let rc = RunConfig{ //! name: Some("echo".into()), +//! provider: None, //! secrets: Some(vec!["api_key".into()]), //! files: None, //! flag: None, diff --git a/src/providers/gopass.rs b/src/providers/gopass.rs index 8f4b68e..a0de4e1 100644 --- a/src/providers/gopass.rs +++ b/src/providers/gopass.rs @@ -18,14 +18,14 @@ use validator::Validate; /// /// Example /// ```no_run -/// use gman::providers::local::GopassProvider; +/// use gman::providers::gopass::GopassProvider; /// use gman::providers::{SecretProvider, SupportedProvider}; /// use gman::config::Config; /// /// let provider = GopassProvider::default(); /// let _ = provider.set_secret("MY_SECRET", "value"); /// ``` -#[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Default, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct GopassProvider { pub store: Option, diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 262a743..f249305 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -6,7 +6,7 @@ pub mod aws_secrets_manager; pub mod azure_key_vault; pub mod gcp_secret_manager; mod git_sync; -mod gopass; +pub mod gopass; pub mod local; use crate::providers::gopass::GopassProvider; diff --git a/tests/config_tests.rs b/tests/config_tests.rs index 7a917bb..1a3a3ee 100644 --- a/tests/config_tests.rs +++ b/tests/config_tests.rs @@ -9,7 +9,7 @@ mod tests { fn test_run_config_valid() { let run_config = RunConfig { name: Some("test".to_string()), - provider: None, + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: None, flag_position: None, @@ -24,7 +24,7 @@ mod tests { fn test_run_config_missing_name() { let run_config = RunConfig { name: None, - provider: None, + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: None, flag_position: None, @@ -39,7 +39,7 @@ mod tests { fn test_run_config_missing_secrets() { let run_config = RunConfig { name: Some("test".to_string()), - provider: None, + provider: None, secrets: None, flag: None, flag_position: None, @@ -54,7 +54,7 @@ mod tests { fn test_run_config_invalid_flag_position() { let run_config = RunConfig { name: Some("test".to_string()), - provider: None, + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: Some("--test-flag".to_string()), flag_position: Some(0), @@ -69,7 +69,7 @@ mod tests { fn test_run_config_flags_or_none_all_some() { let run_config = RunConfig { name: Some("test".to_string()), - provider: None, + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: Some("--test-flag".to_string()), flag_position: Some(1), @@ -84,7 +84,7 @@ mod tests { fn test_run_config_flags_or_none_all_none() { let run_config = RunConfig { name: Some("test".to_string()), - provider: None, + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: None, flag_position: None, @@ -99,7 +99,7 @@ mod tests { fn test_run_config_flags_or_none_partial_some() { let run_config = RunConfig { name: Some("test".to_string()), - provider: None, + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: Some("--test-flag".to_string()), flag_position: None, @@ -114,7 +114,7 @@ mod tests { fn test_run_config_flags_or_none_missing_placeholder() { let run_config = RunConfig { name: Some("test".to_string()), - provider: None, + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: Some("--test-flag".to_string()), flag_position: Some(1), @@ -129,7 +129,7 @@ mod tests { fn test_run_config_flags_or_files_all_none() { let run_config = RunConfig { name: Some("test".to_string()), - provider: None, + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: None, flag_position: None, @@ -144,7 +144,7 @@ mod tests { fn test_run_config_flags_or_files_files_is_some() { let run_config = RunConfig { name: Some("test".to_string()), - provider: None, + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: None, flag_position: None, @@ -159,7 +159,7 @@ mod tests { fn test_run_config_flags_or_files_all_some() { let run_config = RunConfig { name: Some("test".to_string()), - provider: None, + provider: None, secrets: Some(vec!["secret1".to_string()]), flag: Some("--test-flag".to_string()), flag_position: Some(1), diff --git a/tests/providers/gopass_tests.rs b/tests/providers/gopass_tests.rs new file mode 100644 index 0000000..3c6c4b6 --- /dev/null +++ b/tests/providers/gopass_tests.rs @@ -0,0 +1,53 @@ +use gman::config::{Config, ProviderConfig}; +use gman::providers::{SecretProvider, SupportedProvider}; +use pretty_assertions::{assert_eq, assert_str_eq}; +use validator::Validate; + +#[test] +fn test_gopass_supported_provider_display_and_validate_from_yaml() { + // Build a SupportedProvider via YAML to avoid direct type import + let yaml = r#"--- +type: gopass +store: personal +"#; + + let sp: SupportedProvider = serde_yaml::from_str(yaml).expect("valid supported provider yaml"); + // Validate delegates to inner provider (no required fields) + assert!(sp.validate().is_ok()); + // Display formatting for the enum variant + assert_eq!(sp.to_string(), "gopass"); +} + +#[test] +fn test_provider_config_with_gopass_deserialize_and_extract() { + // Minimal ProviderConfig YAML using the gopass variant + let yaml = r#"--- +name: gopass +type: gopass +"#; + + let pc: ProviderConfig = serde_yaml::from_str(yaml).expect("valid provider config yaml"); + // Gopass has no required fields, so validation should pass + assert!(pc.validate().is_ok()); + + // Extract the provider and inspect its name via the trait + let mut pc_owned = pc.clone(); + let provider: &mut dyn SecretProvider = pc_owned.extract_provider(); + assert_str_eq!(provider.name(), "GopassProvider"); + + // Round-trip through Config with default_provider + let cfg_yaml = r#"--- +default_provider: gopass +providers: + - name: gopass + type: gopass + store: personal +"#; + let cfg: Config = serde_yaml::from_str(cfg_yaml).expect("valid config yaml"); + assert!(cfg.validate().is_ok()); + + let extracted = cfg + .extract_provider_config(None) + .expect("should find default provider"); + assert_eq!(extracted.name.as_deref(), Some("gopass")); +} diff --git a/tests/providers/mod.rs b/tests/providers/mod.rs index c8f2ff9..c13f79e 100644 --- a/tests/providers/mod.rs +++ b/tests/providers/mod.rs @@ -1,5 +1,6 @@ mod aws_secrets_manager_tests; mod azure_key_vault_tests; mod gcp_secret_manager_tests; +mod gopass_tests; mod local_tests; mod provider_tests;