diff --git a/Cargo.lock b/Cargo.lock index 05ed759..a6eb923 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -475,6 +475,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -651,12 +657,14 @@ dependencies = [ "indoc", "log", "log4rs", + "pretty_assertions", "regex", "rpassword", "secrecy", "serde", "serde_json", "serde_with", + "tempfile", "thiserror", "validator", "zeroize", @@ -1182,6 +1190,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -2210,6 +2228,12 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 5f0ae6a..8c12c73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "gman" version = "0.1.0" edition = "2024" authors = ["Alex Clarke "] -description = "Universal secret management and command injection tool" +description = "Universal secret management and injection tool" keywords = ["cli", "secrets", "credentials", "command-line", "encryption"] documentation = "https://github.com/Dark-Alex-17/gman" repository = "https://github.com/Dark-Alex-17/gman" @@ -41,19 +41,19 @@ chrono = "0.4.42" indoc = "2.0.6" regex = "1.11.2" +[dev-dependencies] +pretty_assertions = "1.4.1" +tempfile = "3.10.1" + [[bin]] bench = false name = "gman" -[[test]] -name = "mod_tests" -path = "tests/providers/mod_tests.rs" - -[[test]] -name = "local_tests" -path = "tests/providers/local_tests.rs" - [profile.release] lto = true strip = true opt-level = "z" + +[[test]] +name = "integration" +path = "tests/tests.rs" diff --git a/src/bin/gman/cli.rs b/src/bin/gman/cli.rs index ed59e58..6401a0f 100644 --- a/src/bin/gman/cli.rs +++ b/src/bin/gman/cli.rs @@ -1,5 +1,7 @@ use crate::command::preview_command; use anyhow::{Context, Result, anyhow}; +use gman::config::{Config, RunConfig}; +use gman::providers::SecretProvider; use heck::ToSnakeCase; use log::{debug, error}; use regex::Regex; @@ -8,8 +10,6 @@ use std::ffi::OsString; use std::fs; use std::path::PathBuf; use std::process::Command; -use gman::config::{Config, RunConfig}; -use gman::providers::SecretProvider; const ARG_FORMAT_PLACEHOLDER_KEY: &str = "{{key}}"; const ARG_FORMAT_PLACEHOLDER_VALUE: &str = "{{value}}"; @@ -246,32 +246,33 @@ pub fn parse_args( #[cfg(test)] mod tests { - use std::collections::HashMap; - use gman::config::RunConfig; - use crate::cli::generate_files_secret_injections; + use crate::cli::generate_files_secret_injections; + use gman::config::RunConfig; + use pretty_assertions::{assert_eq, assert_str_eq}; + use std::collections::HashMap; - #[test] - fn test_generate_files_secret_injections() { - let mut secrets = HashMap::new(); - secrets.insert("SECRET1".to_string(), "value1".to_string()); - let temp_dir = tempfile::tempdir().unwrap(); - let file_path = temp_dir.path().join("test.txt"); - std::fs::write(&file_path, "{{secret1}}").unwrap(); + #[test] + fn test_generate_files_secret_injections() { + let mut secrets = HashMap::new(); + secrets.insert("SECRET1".to_string(), "value1".to_string()); + let temp_dir = tempfile::tempdir().unwrap(); + let file_path = temp_dir.path().join("test.txt"); + std::fs::write(&file_path, "{{secret1}}").unwrap(); - let run_config = RunConfig { - name: Some("test".to_string()), - secrets: Some(vec!["secret1".to_string()]), - files: Some(vec![file_path.clone()]), - flag: None, - flag_position: None, - arg_format: None, - }; + let run_config = RunConfig { + name: Some("test".to_string()), + secrets: Some(vec!["secret1".to_string()]), + files: Some(vec![file_path.clone()]), + flag: None, + flag_position: None, + arg_format: None, + }; - let result = generate_files_secret_injections(secrets, &run_config).unwrap(); + let result = generate_files_secret_injections(secrets, &run_config).unwrap(); - assert_eq!(result.len(), 1); - assert_eq!(result[0].0, &file_path); - assert_eq!(result[0].1, "{{secret1}}"); - assert_eq!(result[0].2, "value1"); - } + assert_eq!(result.len(), 1); + assert_eq!(result[0].0, &file_path); + assert_str_eq!(result[0].1, "{{secret1}}"); + assert_str_eq!(result[0].2, "value1"); + } } diff --git a/src/bin/gman/command.rs b/src/bin/gman/command.rs index 291de48..187cd2c 100644 --- a/src/bin/gman/command.rs +++ b/src/bin/gman/command.rs @@ -102,3 +102,23 @@ fn ps_quote(s: &OsStr) -> String { s.into_owned() } } + +#[cfg(test)] +mod tests { + use crate::command::preview_command; + use pretty_assertions::assert_str_eq; + use std::process::Command; + + #[test] + fn test_preview_command() { + let mut cmd = Command::new("echo"); + cmd.arg("hello world"); + cmd.env("MY_VAR", "my_value"); + let preview = preview_command(&cmd); + if cfg!(unix) { + assert_str_eq!(preview, "MY_VAR=my_value echo 'hello world'"); + } else if cfg!(windows) { + assert_str_eq!(preview, "set MY_VAR=my_value && \"echo\" \"hello world\""); + } + } +} diff --git a/src/bin/gman/main.rs b/src/bin/gman/main.rs index 8cbe2f8..2fc8e8e 100644 --- a/src/bin/gman/main.rs +++ b/src/bin/gman/main.rs @@ -1,15 +1,15 @@ 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 std::ffi::OsString; use anyhow::{Context, Result}; use clap::Subcommand; use crossterm::execute; -use crossterm::terminal::{disable_raw_mode, LeaveAlternateScreen}; +use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode}; use gman::config::Config; -use gman::providers::local::LocalProvider; use gman::providers::SupportedProvider; +use gman::providers::local::LocalProvider; use heck::ToSnakeCase; use std::io::{self, IsTerminal, Read, Write}; use std::panic::PanicHookInfo; diff --git a/src/bin/gman/utils.rs b/src/bin/gman/utils.rs index bc7d7eb..c2562ee 100644 --- a/src/bin/gman/utils.rs +++ b/src/bin/gman/utils.rs @@ -41,3 +41,20 @@ pub fn get_log_path() -> PathBuf { log_path.push("gman.log"); log_path } + +#[cfg(test)] +mod tests { + use crate::utils::get_log_path; + + #[test] + fn test_get_log_path() { + let log_path = get_log_path(); + if cfg!(target_os = "linux") { + assert!(log_path.ends_with(".cache/gman/gman.log")); + } else if cfg!(target_os = "macos") { + assert!(log_path.ends_with("Library/Logs/gman/gman.log")); + } else if cfg!(target_os = "windows") { + assert!(log_path.ends_with("Logs\\gman\\gman.log")); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 8f8705b..fe292f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -157,6 +157,7 @@ pub fn decrypt_string(password: impl Into, envelope: &str) -> Resu #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn round_trip() { diff --git a/src/providers/local.rs b/src/providers/local.rs index 93d7161..c42d4ad 100644 --- a/src/providers/local.rs +++ b/src/providers/local.rs @@ -330,3 +330,27 @@ fn get_password(config: &Config) -> Result { Ok(SecretString::new(password.into())) } } + +#[cfg(test)] +mod tests { + use crate::derive_key; + use crate::providers::local::derive_key_with_params; + use pretty_assertions::assert_eq; + use secrecy::SecretString; + + #[test] + fn test_derive_key() { + let password = SecretString::new("test_password".to_string().into()); + let salt = [0u8; 16]; + let key = derive_key(&password, &salt).unwrap(); + assert_eq!(key.as_slice().len(), 32); + } + + #[test] + fn test_derive_key_with_params() { + let password = SecretString::new("test_password".to_string().into()); + let salt = [0u8; 16]; + let key = derive_key_with_params(&password, &salt, 10, 1, 1).unwrap(); + assert_eq!(key.as_slice().len(), 32); + } +} diff --git a/tests/bin/gman/main_tests.rs b/tests/bin/gman/main_tests.rs new file mode 100644 index 0000000..b85a3c8 --- /dev/null +++ b/tests/bin/gman/main_tests.rs @@ -0,0 +1,22 @@ +use gman::providers::SupportedProvider; +use gman::providers::local::LocalProvider; +use pretty_assertions::assert_eq; + +#[test] +fn test_provider_kind_from() { + enum ProviderKind { + Local, + } + + impl From for SupportedProvider { + fn from(k: ProviderKind) -> Self { + match k { + ProviderKind::Local => SupportedProvider::Local(LocalProvider), + } + } + } + + let provider_kind = ProviderKind::Local; + let supported_provider: SupportedProvider = provider_kind.into(); + assert_eq!(supported_provider, SupportedProvider::Local(LocalProvider)); +} diff --git a/tests/bin/gman/mod.rs b/tests/bin/gman/mod.rs new file mode 100644 index 0000000..987fc67 --- /dev/null +++ b/tests/bin/gman/mod.rs @@ -0,0 +1 @@ +mod main_tests; diff --git a/tests/bin/mod.rs b/tests/bin/mod.rs new file mode 100644 index 0000000..ad12506 --- /dev/null +++ b/tests/bin/mod.rs @@ -0,0 +1 @@ +mod gman; diff --git a/tests/config_tests.rs b/tests/config_tests.rs index e5c05d3..4213219 100644 --- a/tests/config_tests.rs +++ b/tests/config_tests.rs @@ -3,6 +3,7 @@ mod tests { use gman::config::{Config, RunConfig}; use gman::providers::SupportedProvider; use gman::providers::local::LocalProvider; + use pretty_assertions::{assert_eq, assert_str_eq}; use validator::Validate; @@ -190,7 +191,7 @@ mod tests { fn test_config_extract_provider() { let config = Config::default(); let provider = config.extract_provider(); - assert_eq!(provider.name(), "LocalProvider"); + assert_str_eq!(provider.name(), "LocalProvider"); } #[test] diff --git a/tests/providers/git_sync_tests.rs b/tests/providers/git_sync_tests.rs new file mode 100644 index 0000000..5536ca8 --- /dev/null +++ b/tests/providers/git_sync_tests.rs @@ -0,0 +1,53 @@ +use anyhow::Result; +use validator::Validate; + +// Redefining the struct here for testing purposes +pub struct SyncOpts<'a> { + pub remote_url: &'a Option, + pub branch: &'a Option, +} + +impl<'a> Validate for SyncOpts<'a> { + fn validate(&self) -> Result<(), validator::ValidationErrors> { + if self.remote_url.is_none() { + return Err(validator::ValidationErrors::new()); + } + if self.branch.is_none() { + return Err(validator::ValidationErrors::new()); + } + Ok(()) + } +} + +#[test] +fn test_sync_opts_validation_valid() { + let remote_url = Some("https://github.com/user/repo.git".to_string()); + let branch = Some("main".to_string()); + let opts = SyncOpts { + remote_url: &remote_url, + branch: &branch, + }; + assert!(opts.validate().is_ok()); +} + +#[test] +fn test_sync_opts_validation_missing_remote_url() { + let remote_url = None; + let branch = Some("main".to_string()); + let opts = SyncOpts { + remote_url: &remote_url, + branch: &branch, + }; + assert!(opts.validate().is_err()); +} + +#[test] +fn test_sync_opts_validation_missing_branch() { + let remote_url = Some("https://github.com/user/repo.git".to_string()); + let branch = None; + let opts = SyncOpts { + remote_url: &remote_url, + branch: &branch, + }; + assert!(opts.validate().is_err()); +} diff --git a/tests/providers/local_tests.rs b/tests/providers/local_tests.rs index 95b8d2d..a5bc7a6 100644 --- a/tests/providers/local_tests.rs +++ b/tests/providers/local_tests.rs @@ -1,4 +1,5 @@ use gman::providers::local::LocalProviderConfig; +use pretty_assertions::assert_str_eq; #[test] fn test_local_provider_config_default() { @@ -7,5 +8,14 @@ fn test_local_provider_config_default() { .map(|p| p.join(".gman_vault")) .and_then(|p| p.to_str().map(|s| s.to_string())) .unwrap_or_else(|| ".gman_vault".into()); - assert_eq!(config.vault_path, expected_path); + assert_str_eq!(config.vault_path, expected_path); +} + +#[test] +fn test_local_provider_name() { + use gman::providers::SecretProvider; + use gman::providers::local::LocalProvider; + + let provider = LocalProvider; + assert_str_eq!(provider.name(), "LocalProvider"); } diff --git a/tests/providers/mod.rs b/tests/providers/mod.rs new file mode 100644 index 0000000..52f1f61 --- /dev/null +++ b/tests/providers/mod.rs @@ -0,0 +1,3 @@ +mod git_sync_tests; +mod local_tests; +mod provider_tests; diff --git a/tests/providers/mod_tests.rs b/tests/providers/provider_tests.rs similarity index 55% rename from tests/providers/mod_tests.rs rename to tests/providers/provider_tests.rs index d563287..3564728 100644 --- a/tests/providers/mod_tests.rs +++ b/tests/providers/provider_tests.rs @@ -1,7 +1,29 @@ use gman::providers::local::LocalProvider; use gman::providers::{ParseProviderError, SupportedProvider}; +use pretty_assertions::{assert_eq, assert_str_eq}; use std::str::FromStr; +#[test] +fn test_supported_provider_from_str() { + assert_eq!( + SupportedProvider::from_str("local").unwrap(), + SupportedProvider::Local(LocalProvider) + ); + assert_eq!( + SupportedProvider::from_str(" Local ").unwrap(), + SupportedProvider::Local(LocalProvider) + ); + assert!(matches!( + SupportedProvider::from_str("invalid"), + Err(ParseProviderError::Unsupported(_)) + )); +} + +#[test] +fn test_supported_provider_display() { + assert_str_eq!(SupportedProvider::Local(LocalProvider).to_string(), "local"); +} + #[test] fn test_supported_provider_from_str_valid() { assert_eq!( @@ -17,13 +39,7 @@ fn test_supported_provider_from_str_valid() { #[test] fn test_supported_provider_from_str_invalid() { let err = SupportedProvider::from_str("invalid").unwrap_err(); - assert_eq!(err.to_string(), "unsupported provider 'invalid'"); -} - -#[test] -fn test_supported_provider_display() { - let provider = SupportedProvider::Local(LocalProvider); - assert_eq!(provider.to_string(), "local"); + assert_str_eq!(err.to_string(), "unsupported provider 'invalid'"); } #[test] diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..ca99655 --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,3 @@ +mod bin; +mod config_tests; +mod providers;