diff --git a/Cargo.lock b/Cargo.lock index c6f9e38..0e2954b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -705,6 +705,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.45.0", +] + [[package]] name = "const_format" version = "0.2.32" @@ -895,6 +908,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + [[package]] name = "diff" version = "0.1.13" @@ -923,6 +949,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -1672,8 +1704,10 @@ dependencies = [ "azure_security_keyvault", "clap", "clap_complete", + "console", "convert_case", "crc32c", + "dialoguer", "digest", "enum_dispatch", "env_logger", @@ -2526,6 +2560,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2950,6 +2990,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "unicode-xid" version = "0.2.4" @@ -3244,6 +3290,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -3262,6 +3317,21 @@ dependencies = [ "windows-targets 0.52.0", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -3292,6 +3362,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3304,6 +3380,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3316,6 +3398,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3328,6 +3416,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3340,6 +3434,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3352,6 +3452,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3364,6 +3470,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 3ad66f3..dff50fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,8 @@ digest = "0.10.6" home = "0.5.5" google-secretmanager1 = "5.0.2" is-terminal = "0.4.9" +dialoguer = "0.11.0" +console = "0.15.7" [dependencies.uuid] version = "1.1.2" @@ -55,4 +57,13 @@ features = [ [dev-dependencies] pretty_assertions = "1.3.0" -tempfile = "3.8.1" \ No newline at end of file +tempfile = "3.8.1" + +[profile.test] +incremental = true +opt-level = 0 +debug = 1 +lto = false +debug-assertions = true +overflow-checks = true +rpath = false \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index f1ec4d9..84d689f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,8 @@ use std::collections::HashMap; use schemars::schema_for; use std::process::Command; use std::os::unix::process::CommandExt; +use dialoguer; +use console::Term; #[derive(Debug)] pub struct NovopsLoadArgs { @@ -458,44 +460,58 @@ pub fn check_working_dir_permissions(workdir: &PathBuf) -> Result<(), anyhow::Er } /** - * Prompt user for environment name + * Prompt user for environment name using dialoguer for nice UI */ fn prompt_for_environment(config_file_data: &NovopsConfigFile) -> Result{ - // read config for environments and eventual default environment - let environments = list_environments_from_config(config_file_data); - let default_env_value = String::default(); - let default_env = config_file_data.config.as_ref() - .and_then(|c| c.default.as_ref()) - .and_then(|d| d.environment.as_ref()) - .unwrap_or(&default_env_value); + let environments = config_file_data.environments.iter() + .map(|e| e.0.clone()) + .collect::>(); - // prompt user, show default environment if any - // only show 'default: xxx' if default environment is defined - let mut prompt_msg = format!("Select environment: {:}", environments.join(", ")); + let default = config_file_data.config.clone() + .and_then(|c| c.default) + .and_then(|d| d.environment); - if ! default_env.is_empty() { - prompt_msg.push_str(&format!(" (default: {:})", &default_env)) + // If no environment, no happy + if environments.len() == 0 { + return Err(anyhow::anyhow!("No environment configured.")); + } + + // If only one environment, no need to prompt + if environments.len() == 1 { + debug!("Only one environment configured, using it by default"); + return Ok(environments[0].clone()); + } + + // Look for default environment index in environment list + // to point to this environment by default. + // Error if default environment not found in environment list + let default_index = match default { + Some(d) => { + let idx = environments.iter().position(|e | e == &d); + match idx { + Some(i) => i, + None => { + return Err(anyhow::anyhow!("Default environment '{:}' not found in config", &d)); + }, + } + }, + None => 0, }; - - // use println, we want to prompt user not log something - eprintln!("{prompt_msg}"); - - let mut read_env = String::new(); - std::io::stdin().read_line(&mut read_env) - .with_context(|| "Error reading stdin for environment name user input.")?; - let selected_env = read_env.trim_end().to_string(); + let selection = dialoguer::Select::with_theme(&dialoguer::theme::ColorfulTheme::default()) + .with_prompt("Select environment") + .default(default_index) + .items(&environments) + .interact_on_opt(&Term::stderr()) + .with_context(|| "Couldn't prompt for environment")?; - return if selected_env.is_empty() { - if default_env.is_empty() { - Err(anyhow::anyhow!("No environment selected and no default in config.")) - } else { - Ok(default_env.clone()) - } - } else { - Ok(selected_env) + let selected = match selection { + Some(index) => environments[index].clone(), + None => return Err(anyhow::anyhow!("No environment selected")), }; + + return Ok(selected); } /** diff --git a/tests/test_aws.rs b/tests/test_aws.rs index a0b87cd..e3e46c5 100644 --- a/tests/test_aws.rs +++ b/tests/test_aws.rs @@ -1,141 +1,137 @@ -mod test_utils; +pub mod test_lib; -#[cfg(test)] -mod tests { - use novops::modules::aws::client::{get_ssm_client, get_secretsmanager_client}; - use aws_sdk_ssm::model::ParameterType; - use aws_smithy_types::Blob; - use crate::test_utils::{load_env_for, test_setup, aws_ensure_role_exists, aws_test_config}; +use novops::modules::aws::client::{get_ssm_client, get_secretsmanager_client}; +use aws_sdk_ssm::model::ParameterType; +use aws_smithy_types::Blob; +use test_lib::{load_env_for, test_setup, aws_ensure_role_exists, aws_test_config}; - use log::info; +use log::info; - #[tokio::test] - async fn test_assume_role() -> Result<(), anyhow::Error> { +#[tokio::test] +async fn test_assume_role() -> Result<(), anyhow::Error> { - test_setup().await?; - aws_ensure_role_exists("NovopsTestAwsAssumeRole").await?; + test_setup().await?; + aws_ensure_role_exists("NovopsTestAwsAssumeRole").await?; - let outputs = load_env_for("aws_assumerole", "dev").await?; + let outputs = load_env_for("aws_assumerole", "dev").await?; - info!("test_assume_role: Found variables: {:?}", outputs.variables); + info!("test_assume_role: Found variables: {:?}", outputs.variables); - assert!(outputs.variables.get("AWS_ACCESS_KEY_ID").unwrap().value.len() > 0); - assert!(outputs.variables.get("AWS_SECRET_ACCESS_KEY").unwrap().value.len() > 0); - assert!(outputs.variables.get("AWS_SESSION_TOKEN").unwrap().value.len() > 0); + assert!(outputs.variables.get("AWS_ACCESS_KEY_ID").unwrap().value.len() > 0); + assert!(outputs.variables.get("AWS_SECRET_ACCESS_KEY").unwrap().value.len() > 0); + assert!(outputs.variables.get("AWS_SESSION_TOKEN").unwrap().value.len() > 0); - Ok(()) - } - - #[tokio::test] - async fn test_ssm_param() -> Result<(), anyhow::Error> { + Ok(()) +} - test_setup().await?; +#[tokio::test] +async fn test_ssm_param() -> Result<(), anyhow::Error> { - // String - let pstring_value = "novops-string-test"; - ensure_test_ssm_param_exists("novops-test-ssm-param-string", pstring_value, ParameterType::String).await?; + test_setup().await?; - // SecureString - let psecurestring_value = "novops-string-test-secure"; - ensure_test_ssm_param_exists("novops-test-ssm-param-secureString", psecurestring_value, ParameterType::SecureString).await?; + // String + let pstring_value = "novops-string-test"; + ensure_test_ssm_param_exists("novops-test-ssm-param-string", pstring_value, ParameterType::String).await?; - let outputs = load_env_for("aws_ssm", "dev").await?; + // SecureString + let psecurestring_value = "novops-string-test-secure"; + ensure_test_ssm_param_exists("novops-test-ssm-param-secureString", psecurestring_value, ParameterType::SecureString).await?; - info!("test_ssmparam: Found variables: {:?}", outputs.variables); + let outputs = load_env_for("aws_ssm", "dev").await?; - assert_eq!(outputs.variables.get("SSM_PARAM_STORE_TEST_STRING").unwrap().value, pstring_value); - assert_eq!(outputs.variables.get("SSM_PARAM_STORE_TEST_SECURE_STRING").unwrap().value, psecurestring_value); - assert_ne!(outputs.variables.get("SSM_PARAM_STORE_TEST_SECURE_STRING_NO_DECRYPT").unwrap().value, psecurestring_value); + info!("test_ssmparam: Found variables: {:?}", outputs.variables); - Ok(()) + assert_eq!(outputs.variables.get("SSM_PARAM_STORE_TEST_STRING").unwrap().value, pstring_value); + assert_eq!(outputs.variables.get("SSM_PARAM_STORE_TEST_SECURE_STRING").unwrap().value, psecurestring_value); + assert_ne!(outputs.variables.get("SSM_PARAM_STORE_TEST_SECURE_STRING_NO_DECRYPT").unwrap().value, psecurestring_value); - } + Ok(()) - #[tokio::test] - async fn test_secretsmanager() -> Result<(), anyhow::Error> { +} - // Prepare env and dummy secret - test_setup().await?; +#[tokio::test] +async fn test_secretsmanager() -> Result<(), anyhow::Error> { - let expect_string = "Some-String-data?1548a~#{[[".to_string(); - let expect_binary = vec![240, 159, 146, 150]; // 💖 - ensure_test_secret_exists("novops-test-secretsmanager-string", Some(expect_string.clone()), None).await?; - ensure_test_secret_exists("novops-test-secretsmanager-binary", None, Some(expect_binary.clone())).await?; - - let outputs = load_env_for("aws_secretsmanager", "dev").await?; + // Prepare env and dummy secret + test_setup().await?; - info!("test_secretsmanager: Found variables: {:?}", outputs.variables); - info!("test_secretsmanager: Found files: {:?}", outputs.files); + let expect_string = "Some-String-data?1548a~#{[[".to_string(); + let expect_binary = vec![240, 159, 146, 150]; // 💖 + ensure_test_secret_exists("novops-test-secretsmanager-string", Some(expect_string.clone()), None).await?; + ensure_test_secret_exists("novops-test-secretsmanager-binary", None, Some(expect_binary.clone())).await?; + + let outputs = load_env_for("aws_secretsmanager", "dev").await?; - assert_eq!(outputs.variables.get("SECRETSMANAGER_VAR_STRING").unwrap().value, expect_string); - assert_eq!(outputs.variables.get("SECRETSMANAGER_VAR_BINARY").unwrap().value.as_bytes(), expect_binary); - assert_eq!(outputs.files.get("/tmp/SECRETSMANAGER_FILE_STRING").unwrap().content, expect_string.as_bytes()); - assert_eq!(outputs.files.get("/tmp/SECRETSMANAGER_FILE_BINARY").unwrap().content, expect_binary); + info!("test_secretsmanager: Found variables: {:?}", outputs.variables); + info!("test_secretsmanager: Found files: {:?}", outputs.files); - Ok(()) - } + assert_eq!(outputs.variables.get("SECRETSMANAGER_VAR_STRING").unwrap().value, expect_string); + assert_eq!(outputs.variables.get("SECRETSMANAGER_VAR_BINARY").unwrap().value.as_bytes(), expect_binary); + assert_eq!(outputs.files.get("/tmp/SECRETSMANAGER_FILE_STRING").unwrap().content, expect_string.as_bytes()); + assert_eq!(outputs.files.get("/tmp/SECRETSMANAGER_FILE_BINARY").unwrap().content, expect_binary); - /** - * Variable input cannot be used for NON-UTF8 data yet. Let's test for proper error message. - */ - #[tokio::test] - async fn test_secretsmanager_non_utf8_variable() -> Result<(), anyhow::Error> { + Ok(()) +} - // Prepare env and dummy secret - test_setup().await?; +/** + * Variable input cannot be used for NON-UTF8 data yet. Let's test for proper error message. + */ +#[tokio::test] +async fn test_secretsmanager_non_utf8_variable() -> Result<(), anyhow::Error> { - let non_utf8_binary = vec![0, 159, 146, 150]; - ensure_test_secret_exists("novops-test-secretsmanager-binary-non-utf8", None, Some(non_utf8_binary.clone())).await?; - - let outputs = load_env_for("aws_secretsmanager_var_nonutf8", "dev").await; - outputs.expect_err("Expected non UTF-8 binary data to provoke an error."); + // Prepare env and dummy secret + test_setup().await?; - Ok(()) - } + let non_utf8_binary = vec![0, 159, 146, 150]; + ensure_test_secret_exists("novops-test-secretsmanager-binary-non-utf8", None, Some(non_utf8_binary.clone())).await?; + + let outputs = load_env_for("aws_secretsmanager_var_nonutf8", "dev").await; + outputs.expect_err("Expected non UTF-8 binary data to provoke an error."); - async fn ensure_test_ssm_param_exists(pname: &str, pvalue: &str, ptype: ParameterType) -> Result<(), anyhow::Error> { - let client = get_ssm_client(&aws_test_config()).await?; + Ok(()) +} - info!("PUT SSM param: {}", pname); +async fn ensure_test_ssm_param_exists(pname: &str, pvalue: &str, ptype: ParameterType) -> Result<(), anyhow::Error> { + let client = get_ssm_client(&aws_test_config()).await?; - let r = client.put_parameter() - .name(pname) - .overwrite(true) - .value(pvalue) - .r#type(ptype) - .send().await?; + info!("PUT SSM param: {}", pname); - info!("SSM param PUT {} response: {:?}", pname, r); + let r = client.put_parameter() + .name(pname) + .overwrite(true) + .value(pvalue) + .r#type(ptype) + .send().await?; - Ok(()) - } + info!("SSM param PUT {} response: {:?}", pname, r); - async fn ensure_test_secret_exists(sname: &str, string_value: Option, binary_value: Option>) - -> Result<(), anyhow::Error> { - let client = get_secretsmanager_client(&aws_test_config()).await?; + Ok(()) +} - match client.describe_secret().secret_id(sname).send().await { - Ok(r) => { - info!("Secret {} already exists, deleting...", sname); +async fn ensure_test_secret_exists(sname: &str, string_value: Option, binary_value: Option>) + -> Result<(), anyhow::Error> { + let client = get_secretsmanager_client(&aws_test_config()).await?; - client.delete_secret() - .secret_id(r.arn().unwrap()) - .force_delete_without_recovery(true) - .send().await?; - }, - Err(_) => {}, // secret does not exists, ignore - } + match client.describe_secret().secret_id(sname).send().await { + Ok(r) => { + info!("Secret {} already exists, deleting...", sname); - let r = client.create_secret() - .name(sname) - .set_secret_string(string_value) - .set_secret_binary(binary_value.map(|b| Blob::new(b))) - .send().await?; + client.delete_secret() + .secret_id(r.arn().unwrap()) + .force_delete_without_recovery(true) + .send().await?; + }, + Err(_) => {}, // secret does not exists, ignore + } - info!("Create AWS secret {} response: {:?}", sname, r); + let r = client.create_secret() + .name(sname) + .set_secret_string(string_value) + .set_secret_binary(binary_value.map(|b| Blob::new(b))) + .send().await?; - Ok(()) - } + info!("Create AWS secret {} response: {:?}", sname, r); -} \ No newline at end of file + Ok(()) +} diff --git a/tests/test_azure.rs b/tests/test_azure.rs index 6a9fdd8..3b7f29f 100644 --- a/tests/test_azure.rs +++ b/tests/test_azure.rs @@ -1,25 +1,19 @@ -mod test_utils; +mod test_lib; +use test_lib::{load_env_dryrun_for, test_setup}; +use log::info; -#[cfg(test)] -mod tests { - use crate::test_utils; +#[tokio::test] +async fn test_azure_keyvault() -> Result<(), anyhow::Error> { - use log::info; + test_setup().await?; - #[tokio::test] - async fn test_azure_keyvault() -> Result<(), anyhow::Error> { + let outputs = load_env_dryrun_for("azure_keyvault", "dev").await?; - test_utils::test_setup().await?; + info!("test_azure_keyvault: Found variables: {:?}", outputs.variables); + info!("test_azure_keyvault: Found files: {:?}", outputs.files); - let outputs = test_utils::load_env_dryrun_for("azure_keyvault", "dev").await?; - - info!("test_azure_keyvault: Found variables: {:?}", outputs.variables); - info!("test_azure_keyvault: Found files: {:?}", outputs.files); - - assert_eq!(outputs.variables.get("AZ_KEYVAULT_SECRET_VAR").unwrap().value, "RESULT:novops-test-kv/test-secret/"); - assert_eq!(outputs.files.get("/tmp/AZ_KEYVAULT_SECRET_FILE").unwrap().content, "RESULT:novops-test-kv/test-secret/56ed118a41364a9e8a086e76c43629e4".as_bytes()); - Ok(()) - } - -} \ No newline at end of file + assert_eq!(outputs.variables.get("AZ_KEYVAULT_SECRET_VAR").unwrap().value, "RESULT:novops-test-kv/test-secret/"); + assert_eq!(outputs.files.get("/tmp/AZ_KEYVAULT_SECRET_FILE").unwrap().content, "RESULT:novops-test-kv/test-secret/56ed118a41364a9e8a086e76c43629e4".as_bytes()); + Ok(()) +} diff --git a/tests/test_core.rs b/tests/test_core.rs index 3d251e7..7f23e04 100644 --- a/tests/test_core.rs +++ b/tests/test_core.rs @@ -1,366 +1,357 @@ +mod test_lib; + +use novops::modules::variables::VariableOutput; +use novops::{make_context, NovopsLoadArgs, + load_environment_write_vars, prepare_exec_command, should_error_tty, + list_environments, list_outputs_for_environment, check_working_dir_permissions}; +use novops::core::{NovopsContext, NovopsConfig, NovopsConfigFile, NovopsConfigDefault, NovopsEnvironmentInput}; +use std::collections::HashMap; +use std::ffi::OsStr; +use std::path::PathBuf; +use std::fs::{self, Permissions}; +use std::os::unix::fs::PermissionsExt; +use log::info; +use test_lib::{clean_and_setup_test_dir, TEST_DIR, load_env_dryrun_for, test_setup}; + +const CONFIG_EMPTY: &str = "tests/.novops.empty.yml"; +const CONFIG_STANDALONE: &str = "tests/.novops.plain-strings.yml"; + +/** + * Test a config is properly loaded into a NovopsContext + */ +#[tokio::test] +async fn test_load_simple_config() -> Result<(), anyhow::Error>{ + test_setup().await?; + + let workdir = clean_and_setup_test_dir("test_load_simple_config")?; + + let args = NovopsLoadArgs { + config: String::from(CONFIG_EMPTY), + env: Some(String::from("dev")), + working_directory: Some(workdir.clone().into_os_string().into_string().unwrap()), + skip_working_directory_check: Some(false), + dry_run: None + }; + let result = make_context(&args).await?; + + info!("Result: {:?}", result); + + assert_eq!(result, + NovopsContext { + env_name: String::from("dev"), + app_name: String::from("test-empty"), + workdir: workdir.clone(), + config_file_data: NovopsConfigFile{ + name: Some(String::from("test-empty")), + environments: HashMap::from([ + (String::from("dev"), NovopsEnvironmentInput { + variables: None, + files: None, + aws: None, + hashivault: None, + sops_dotenv: None, + }) + ]), + config: Some(NovopsConfig { + default: Some(NovopsConfigDefault { + environment: Some(String::from("dev")) + }), + hashivault: None, + aws: None + }) + }, + env_var_filepath: workdir.join("vars"), + dry_run: false + } + ); + + Ok(()) +} + + +/** + * Run Novops and check expected files and variables are generated: + * - var file with expected export value (in any order) + * - Generated files and content + */ +#[tokio::test] +async fn test_simple_run() -> Result<(), anyhow::Error>{ + test_setup().await?; + + let workdir = clean_and_setup_test_dir("test_simple_run")?; + + load_environment_write_vars(&NovopsLoadArgs { + config: String::from(CONFIG_STANDALONE), + env: Some(String::from("dev")), + working_directory: Some(workdir.clone().into_os_string().into_string().unwrap()), + skip_working_directory_check: Some(false), + dry_run: None + }, + &Some(String::from(".envrc")), + &String::from("dotenv-export"), + true + ).await?; + + let expected_var_file = PathBuf::from(&workdir).join("vars"); + let expected_var_content = fs::read_to_string(expected_var_file)?; + + let expected_file_dog_path = PathBuf::from(&workdir).join("file_1811bdd29f2cfe95e6e23402e2390fa1012708fc52ef8b8a29ee540b1c481534"); + let expected_file_dog_content = fs::read_to_string(&expected_file_dog_path)?; + let file_dog_metadata = fs::metadata(&expected_file_dog_path)?; + let file_dog_mode = file_dog_metadata.permissions().mode(); + + let expected_file_cat_path = PathBuf::from("/tmp/novops_cat"); + let expected_file_cat_content = fs::read_to_string(&expected_file_cat_path)?; + + // Expect to match content of CONFIG_STANDALONE + // use r#"_"# for raw string literal + // check if our file content contains expected export + // naïve but sufficient for our needs + assert!(&expected_var_content.contains(r#"export SPECIAL_CHARACTERS='special_char_'"'"'!?`$abc_#~%*µ€{}[]-°+@à^ç=\'"#)); + assert!(&expected_var_content.contains( "export MY_APP_HOST='localhost'")); + assert!(&expected_var_content.contains( &format!("export DOG_PATH='{:}'", + &expected_file_dog_path.clone().into_os_string().into_string().unwrap()))); + assert!(&expected_var_content.contains( "export NOVOPS_CAT_VAR='/tmp/novops_cat'")); + + // expect file permission to be 0600 (user readonly) + // use a bitwise AND on ocal value to check for user-only permission 0600 + assert_eq!(file_dog_metadata.permissions().mode() & 0o777, 0o600, "Expected {:?} to have permission {:o}, found {:o}", + &expected_file_dog_path, 0o600, &file_dog_mode); + + + assert_eq!(expected_file_dog_content, "woof"); + assert_eq!(expected_file_cat_content, "meow"); + + Ok(()) + +} + +#[tokio::test] +async fn test_symlink_flag() -> Result<(), anyhow::Error> { + test_setup().await?; + + let workdir = clean_and_setup_test_dir("test_symlink_flag")?; -#[cfg(test)] -mod test_utils; - -#[cfg(test)] -mod tests { - use novops::modules::variables::VariableOutput; - use novops::{make_context, NovopsLoadArgs, - load_environment_write_vars, prepare_exec_command, should_error_tty, - list_environments, list_outputs_for_environment, check_working_dir_permissions}; - use novops::core::{NovopsContext, NovopsConfig, NovopsConfigFile, NovopsConfigDefault, NovopsEnvironmentInput}; - use std::collections::HashMap; - use std::ffi::OsStr; - use std::path::PathBuf; - use std::fs::{self, Permissions}; - use std::os::unix::fs::PermissionsExt; - use log::info; - use crate::test_utils::{clean_and_setup_test_dir, TEST_DIR, load_env_dryrun_for, test_setup}; - - const CONFIG_EMPTY: &str = "tests/.novops.empty.yml"; - const CONFIG_STANDALONE: &str = "tests/.novops.plain-strings.yml"; - - /** - * Test a config is properly loaded into a NovopsContext - */ - #[tokio::test] - async fn test_load_simple_config() -> Result<(), anyhow::Error>{ - test_setup().await?; - - let workdir = clean_and_setup_test_dir("test_load_simple_config")?; - - let args = NovopsLoadArgs { - config: String::from(CONFIG_EMPTY), + let expect_symlink_at = PathBuf::from(TEST_DIR).join("test-symlink"); + load_environment_write_vars(&NovopsLoadArgs { + config: String::from(CONFIG_STANDALONE), env: Some(String::from("dev")), working_directory: Some(workdir.clone().into_os_string().into_string().unwrap()), skip_working_directory_check: Some(false), dry_run: None - }; - let result = make_context(&args).await?; - - info!("Result: {:?}", result); - - assert_eq!(result, - NovopsContext { - env_name: String::from("dev"), - app_name: String::from("test-empty"), - workdir: workdir.clone(), - config_file_data: NovopsConfigFile{ - name: Some(String::from("test-empty")), - environments: HashMap::from([ - (String::from("dev"), NovopsEnvironmentInput { - variables: None, - files: None, - aws: None, - hashivault: None, - sops_dotenv: None, - }) - ]), - config: Some(NovopsConfig { - default: Some(NovopsConfigDefault { - environment: Some(String::from("dev")) - }), - hashivault: None, - aws: None - }) - }, - env_var_filepath: workdir.join("vars"), - dry_run: false - } - ); + }, + &Some(expect_symlink_at.clone().into_os_string().into_string().unwrap()), + &String::from("dotenv-export"), + true + ).await?; + + let symlink_metadata = fs::symlink_metadata(&expect_symlink_at)?; + assert!(symlink_metadata.is_symlink(), "{:?} does not seem to be a symlink: {:?}", &expect_symlink_at, symlink_metadata); + + // symlink is expected to point to var file under our working directory + let symlink_dest = fs::read_link(&expect_symlink_at).unwrap(); + assert_eq!(symlink_dest, PathBuf::from(&workdir).join("vars"), "Symlink destination is not as expected"); + + // run again with different symlink dest + // expect existing symlink to be overriden + let workdir_override = clean_and_setup_test_dir("test_symlink_flag_override")?; + load_environment_write_vars(&NovopsLoadArgs { + config: String::from(CONFIG_STANDALONE), + env: Some(String::from("staging")), + working_directory: Some(workdir_override.clone().into_os_string().into_string().unwrap()), + skip_working_directory_check: Some(false), + dry_run: None + }, + &Some(expect_symlink_at.clone().into_os_string().into_string().unwrap()), + &String::from("dotenv-export"), + true + ).await?; - Ok(()) - } + let overriden_symlink_dest = fs::read_link(&expect_symlink_at).unwrap(); + assert_eq!(overriden_symlink_dest, PathBuf::from(&workdir_override).join("vars"), "Symlink destination is not as expected"); + Ok(()) +} - /** - * Run Novops and check expected files and variables are generated: - * - var file with expected export value (in any order) - * - Generated files and content - */ - #[tokio::test] - async fn test_simple_run() -> Result<(), anyhow::Error>{ - test_setup().await?; +/** + * Ensure that a file/dir at symlink path result in failure + */ +#[tokio::test] +async fn test_symlink_no_file_override() -> Result<(), anyhow::Error> { + test_setup().await?; - let workdir = clean_and_setup_test_dir("test_simple_run")?; + let workdir = clean_and_setup_test_dir("test_symlink_no_file_override")?; - load_environment_write_vars(&NovopsLoadArgs { - config: String::from(CONFIG_STANDALONE), - env: Some(String::from("dev")), - working_directory: Some(workdir.clone().into_os_string().into_string().unwrap()), - skip_working_directory_check: Some(false), - dry_run: None - }, - &Some(String::from(".envrc")), - &String::from("dotenv-export"), - true - ).await?; - - let expected_var_file = PathBuf::from(&workdir).join("vars"); - let expected_var_content = fs::read_to_string(expected_var_file)?; - - let expected_file_dog_path = PathBuf::from(&workdir).join("file_1811bdd29f2cfe95e6e23402e2390fa1012708fc52ef8b8a29ee540b1c481534"); - let expected_file_dog_content = fs::read_to_string(&expected_file_dog_path)?; - let file_dog_metadata = fs::metadata(&expected_file_dog_path)?; - let file_dog_mode = file_dog_metadata.permissions().mode(); - - let expected_file_cat_path = PathBuf::from("/tmp/novops_cat"); - let expected_file_cat_content = fs::read_to_string(&expected_file_cat_path)?; - - // Expect to match content of CONFIG_STANDALONE - // use r#"_"# for raw string literal - // check if our file content contains expected export - // naïve but sufficient for our needs - assert!(&expected_var_content.contains(r#"export SPECIAL_CHARACTERS='special_char_'"'"'!?`$abc_#~%*µ€{}[]-°+@à^ç=\'"#)); - assert!(&expected_var_content.contains( "export MY_APP_HOST='localhost'")); - assert!(&expected_var_content.contains( &format!("export DOG_PATH='{:}'", - &expected_file_dog_path.clone().into_os_string().into_string().unwrap()))); - assert!(&expected_var_content.contains( "export NOVOPS_CAT_VAR='/tmp/novops_cat'")); - - // expect file permission to be 0600 (user readonly) - // use a bitwise AND on ocal value to check for user-only permission 0600 - assert_eq!(file_dog_metadata.permissions().mode() & 0o777, 0o600, "Expected {:?} to have permission {:o}, found {:o}", - &expected_file_dog_path, 0o600, &file_dog_mode); - - - assert_eq!(expected_file_dog_content, "woof"); - assert_eq!(expected_file_cat_content, "meow"); - - Ok(()) - - } + // create dummy file, we don't want it erased by symlink + let symlink_path = PathBuf::from(&workdir).join("file-dont-override"); + fs::File::create(&symlink_path)?; - #[tokio::test] - async fn test_symlink_flag() -> Result<(), anyhow::Error> { - test_setup().await?; - - let workdir = clean_and_setup_test_dir("test_symlink_flag")?; - - let expect_symlink_at = PathBuf::from(TEST_DIR).join("test-symlink"); - load_environment_write_vars(&NovopsLoadArgs { - config: String::from(CONFIG_STANDALONE), - env: Some(String::from("dev")), - working_directory: Some(workdir.clone().into_os_string().into_string().unwrap()), - skip_working_directory_check: Some(false), - dry_run: None - }, - &Some(expect_symlink_at.clone().into_os_string().into_string().unwrap()), - &String::from("dotenv-export"), - true - ).await?; - - let symlink_metadata = fs::symlink_metadata(&expect_symlink_at)?; - assert!(symlink_metadata.is_symlink(), "{:?} does not seem to be a symlink: {:?}", &expect_symlink_at, symlink_metadata); - - // symlink is expected to point to var file under our working directory - let symlink_dest = fs::read_link(&expect_symlink_at).unwrap(); - assert_eq!(symlink_dest, PathBuf::from(&workdir).join("vars"), "Symlink destination is not as expected"); - - // run again with different symlink dest - // expect existing symlink to be overriden - let workdir_override = clean_and_setup_test_dir("test_symlink_flag_override")?; - load_environment_write_vars(&NovopsLoadArgs { - config: String::from(CONFIG_STANDALONE), - env: Some(String::from("staging")), - working_directory: Some(workdir_override.clone().into_os_string().into_string().unwrap()), - skip_working_directory_check: Some(false), - dry_run: None - }, - &Some(expect_symlink_at.clone().into_os_string().into_string().unwrap()), - &String::from("dotenv-export"), - true - ).await?; - - let overriden_symlink_dest = fs::read_link(&expect_symlink_at).unwrap(); - assert_eq!(overriden_symlink_dest, PathBuf::from(&workdir_override).join("vars"), "Symlink destination is not as expected"); + // expect error as we cannot erase existing file + let result = load_environment_write_vars(&NovopsLoadArgs { + config: String::from(CONFIG_STANDALONE), + env: Some(String::from("dev")), + working_directory: Some(workdir.clone().into_os_string().into_string().unwrap()), + skip_working_directory_check: Some(false), + dry_run: None + }, + &Some(symlink_path.clone().into_os_string().into_string().unwrap()), + &String::from("dotenv-export"), + true + ).await; - Ok(()) - } + result.expect_err("Expected an error when loading with symlink trying to override existing file, got OK."); - /** - * Ensure that a file/dir at symlink path result in failure - */ - #[tokio::test] - async fn test_symlink_no_file_override() -> Result<(), anyhow::Error> { - test_setup().await?; - - let workdir = clean_and_setup_test_dir("test_symlink_no_file_override")?; - - // create dummy file, we don't want it erased by symlink - let symlink_path = PathBuf::from(&workdir).join("file-dont-override"); - fs::File::create(&symlink_path)?; - - // expect error as we cannot erase existing file - let result = load_environment_write_vars(&NovopsLoadArgs { - config: String::from(CONFIG_STANDALONE), - env: Some(String::from("dev")), - working_directory: Some(workdir.clone().into_os_string().into_string().unwrap()), - skip_working_directory_check: Some(false), - dry_run: None - }, - &Some(symlink_path.clone().into_os_string().into_string().unwrap()), - &String::from("dotenv-export"), - true - ).await; - - result.expect_err("Expected an error when loading with symlink trying to override existing file, got OK."); - - Ok(()) - } + Ok(()) +} - /** - * Check all modules with dry run - * Having non-empty values and no errors is enough - */ - #[tokio::test] - async fn test_dry_run() -> Result<(), anyhow::Error> { - test_setup().await?; - - let result = load_env_dryrun_for("all-modules", "dev").await?; - - info!("test_dry_run: Found variables: {:?}", &result.variables); - info!("test_dry_run: Found files: {:?}", &result.files); - - - assert!(result.variables.get("VAR").unwrap().value.len() > 0); - assert!(result.variables.get("AWS_SECRETMANAGER").unwrap().value.len() > 0); - assert!(result.variables.get("AWS_SSM_PARAMETER").unwrap().value.len() > 0); - assert!(result.variables.get("HASHIVAULT_KV_V2").unwrap().value.len() > 0); - assert!(result.variables.get("BITWARDEN").unwrap().value.len() > 0); - assert!(result.variables.get("GCLOUD_SECRETMANAGER").unwrap().value.len() > 0); - assert!(result.files.get("/tmp/novopsfile").unwrap().content.len() > 0); +/** + * Check all modules with dry run + * Having non-empty values and no errors is enough + */ +#[tokio::test] +async fn test_dry_run() -> Result<(), anyhow::Error> { + test_setup().await?; - // aws.assumerole - assert!(result.variables.get("AWS_ACCESS_KEY_ID").unwrap().value.len() > 0); - assert!(result.variables.get("AWS_SESSION_TOKEN").unwrap().value.len() > 0); - assert!(result.variables.get("AWS_SECRET_ACCESS_KEY").unwrap().value.len() > 0); + let result = load_env_dryrun_for("all-modules", "dev").await?; - Ok(()) - } + info!("test_dry_run: Found variables: {:?}", &result.variables); + info!("test_dry_run: Found files: {:?}", &result.files); - /** - * Check all modules with dry run - * Having non-empty values and no errors is enough - */ - #[tokio::test] - async fn test_run_prepare_process() -> Result<(), anyhow::Error> { - test_setup().await?; + assert!(result.variables.get("VAR").unwrap().value.len() > 0); + assert!(result.variables.get("AWS_SECRETMANAGER").unwrap().value.len() > 0); + assert!(result.variables.get("AWS_SSM_PARAMETER").unwrap().value.len() > 0); + assert!(result.variables.get("HASHIVAULT_KV_V2").unwrap().value.len() > 0); + assert!(result.variables.get("BITWARDEN").unwrap().value.len() > 0); + assert!(result.variables.get("GCLOUD_SECRETMANAGER").unwrap().value.len() > 0); + assert!(result.files.get("/tmp/novopsfile").unwrap().content.len() > 0); - let cmd =String::from("sh"); - let arg1 =String::from("-c"); - let arg2 =String::from("echo foo"); - let args = vec![&cmd, &arg1, &arg2]; + // aws.assumerole + assert!(result.variables.get("AWS_ACCESS_KEY_ID").unwrap().value.len() > 0); + assert!(result.variables.get("AWS_SESSION_TOKEN").unwrap().value.len() > 0); + assert!(result.variables.get("AWS_SECRET_ACCESS_KEY").unwrap().value.len() > 0); - let var1 = "FOO"; - let val1 = "barzzz"; - let vars : Vec = vec![VariableOutput{ - name: String::from(var1), - value: String::from(val1) - }]; + Ok(()) +} - let result = prepare_exec_command(args, &vars); - - assert_eq!(result.get_envs().len(), 1); +/** + * Check all modules with dry run + * Having non-empty values and no errors is enough + */ +#[tokio::test] +async fn test_run_prepare_process() -> Result<(), anyhow::Error> { + test_setup().await?; - let os_vars : Vec<(&OsStr, Option<&OsStr>)> = result.get_envs().collect(); - assert_eq!(os_vars[0], (OsStr::new(var1), Some(OsStr::new(val1)))); - assert_eq!(result.get_program(), OsStr::new("sh")); + let cmd =String::from("sh"); + let arg1 =String::from("-c"); + let arg2 =String::from("echo foo"); + let args = vec![&cmd, &arg1, &arg2]; - let result_args : Vec<&OsStr> = result.get_args().map(|arg| arg).collect(); - assert_eq!(result_args, vec![OsStr::new(&arg1), OsStr::new(&arg2)]); - - Ok(()) - } + let var1 = "FOO"; + let val1 = "barzzz"; + let vars : Vec = vec![VariableOutput{ + name: String::from(var1), + value: String::from(val1) + }]; - #[tokio::test] - async fn test_should_error_tty() -> Result<(), anyhow::Error> { + let result = prepare_exec_command(args, &vars); + + assert_eq!(result.get_envs().len(), 1); - let symlink_none = None; - let symlink_some = Some(String::from(".envrc")); + let os_vars : Vec<(&OsStr, Option<&OsStr>)> = result.get_envs().collect(); + assert_eq!(os_vars[0], (OsStr::new(var1), Some(OsStr::new(val1)))); - // terminal is tty - assert_eq!(should_error_tty(true, true, &symlink_none), false, "Skipped tty check should not provoke failsafe"); - assert_eq!(should_error_tty(true, true, &symlink_some), false, "Skipped tty check should not provoke failsafe"); - assert_eq!(should_error_tty(true, false, &symlink_none), true, "tty terminal without symlink should provoke failsafe"); - assert_eq!(should_error_tty(true, false, &symlink_some), false, "tty terminal with symlink should not provoke failsafe"); + assert_eq!(result.get_program(), OsStr::new("sh")); - // terminal is NOT tty - assert_eq!(should_error_tty(false, true, &symlink_none), false, "Non-tty terminal should not cause failsafe"); - assert_eq!(should_error_tty(false, true, &symlink_some), false, "Non-tty terminal should not cause failsafe"); - assert_eq!(should_error_tty(false, false, &symlink_none), false, "Non-tty terminal should not cause failsafe"); - assert_eq!(should_error_tty(false, false, &symlink_some), false, "Non-tty terminal should not cause failsafe"); + let result_args : Vec<&OsStr> = result.get_args().map(|arg| arg).collect(); + assert_eq!(result_args, vec![OsStr::new(&arg1), OsStr::new(&arg2)]); + + Ok(()) +} - Ok(()) - } +#[tokio::test] +async fn test_should_error_tty() -> Result<(), anyhow::Error> { - #[tokio::test] - async fn test_default_loaded_vars() -> Result<(), anyhow::Error> { + let symlink_none = None; + let symlink_some = Some(String::from(".envrc")); - let result = load_env_dryrun_for("empty", "dev").await?; + // terminal is tty + assert_eq!(should_error_tty(true, true, &symlink_none), false, "Skipped tty check should not provoke failsafe"); + assert_eq!(should_error_tty(true, true, &symlink_some), false, "Skipped tty check should not provoke failsafe"); + assert_eq!(should_error_tty(true, false, &symlink_none), true, "tty terminal without symlink should provoke failsafe"); + assert_eq!(should_error_tty(true, false, &symlink_some), false, "tty terminal with symlink should not provoke failsafe"); - assert_eq!(result.variables.get("NOVOPS_ENVIRONMENT").unwrap().value, "dev"); + // terminal is NOT tty + assert_eq!(should_error_tty(false, true, &symlink_none), false, "Non-tty terminal should not cause failsafe"); + assert_eq!(should_error_tty(false, true, &symlink_some), false, "Non-tty terminal should not cause failsafe"); + assert_eq!(should_error_tty(false, false, &symlink_none), false, "Non-tty terminal should not cause failsafe"); + assert_eq!(should_error_tty(false, false, &symlink_some), false, "Non-tty terminal should not cause failsafe"); - Ok(()) - } + Ok(()) +} - #[tokio::test] - async fn test_list_environments() -> Result<(), anyhow::Error> { - test_setup().await?; +#[tokio::test] +async fn test_default_loaded_vars() -> Result<(), anyhow::Error> { - let result = list_environments("tests/.novops.multi-env.yml").await?; + let result = load_env_dryrun_for("empty", "dev").await?; - assert_eq!(result.len(), 4); - assert_eq!(result[0], "dev"); - assert_eq!(result[1], "preprod"); - assert_eq!(result[2], "prod"); - assert_eq!(result[3], "staging"); - Ok(()) - } + assert_eq!(result.variables.get("NOVOPS_ENVIRONMENT").unwrap().value, "dev"); - #[tokio::test] - async fn test_list_environment_output() -> Result<(), anyhow::Error> { - test_setup().await?; + Ok(()) +} - let result = list_outputs_for_environment("tests/.novops.multi-env.yml", Some("dev".to_string())).await?; +#[tokio::test] +async fn test_list_environments() -> Result<(), anyhow::Error> { + test_setup().await?; - // Assert this - assert_eq!(result.variables.len(), 3); - assert_eq!(result.variables.get("MY_APP_HOST").unwrap().value, "localhost"); + let result = list_environments("tests/.novops.multi-env.yml").await?; - assert_eq!(result.files.len(), 1); + assert_eq!(result.len(), 4); + assert_eq!(result[0], "dev"); + assert_eq!(result[1], "preprod"); + assert_eq!(result[2], "prod"); + assert_eq!(result[3], "staging"); + Ok(()) +} - Ok(()) - } +#[tokio::test] +async fn test_list_environment_output() -> Result<(), anyhow::Error> { + test_setup().await?; - #[tokio::test] - async fn check_working_dir_permissions_test() -> Result<(), anyhow::Error> { + let result = list_outputs_for_environment("tests/.novops.multi-env.yml", Some("dev".to_string())).await?; - fn make_tmp_dir(mode: u32) -> PathBuf { - let dir = tempfile::tempdir().unwrap().into_path(); - let perm = Permissions::from_mode(mode); - fs::set_permissions(&dir, perm).unwrap(); - return dir - } + // Assert this + assert_eq!(result.variables.len(), 3); + assert_eq!(result.variables.get("MY_APP_HOST").unwrap().value, "localhost"); - let dir_user = make_tmp_dir(0o700); - let dir_group = make_tmp_dir(0o760); - let dir_world = make_tmp_dir(0o706); - - let result_user = check_working_dir_permissions(&dir_user); - assert!(result_user.is_ok(), "Directory with user-only permission should pass check, got {:?}", result_user); + assert_eq!(result.files.len(), 1); - let result_group = check_working_dir_permissions(&dir_group); - assert!(result_group.is_err(), "Directory with group permission should not pass check, got {:?}", result_group); + Ok(()) +} - let result_world = check_working_dir_permissions(&dir_world); - assert!(result_world.is_err(), "Directory with world permission should not pass check, got {:?}", result_world); +#[tokio::test] +async fn check_working_dir_permissions_test() -> Result<(), anyhow::Error> { - Ok(()) + fn make_tmp_dir(mode: u32) -> PathBuf { + let dir = tempfile::tempdir().unwrap().into_path(); + let perm = Permissions::from_mode(mode); + fs::set_permissions(&dir, perm).unwrap(); + return dir } + let dir_user = make_tmp_dir(0o700); + let dir_group = make_tmp_dir(0o760); + let dir_world = make_tmp_dir(0o706); + + let result_user = check_working_dir_permissions(&dir_user); + assert!(result_user.is_ok(), "Directory with user-only permission should pass check, got {:?}", result_user); + let result_group = check_working_dir_permissions(&dir_group); + assert!(result_group.is_err(), "Directory with group permission should not pass check, got {:?}", result_group); -} + let result_world = check_working_dir_permissions(&dir_world); + assert!(result_world.is_err(), "Directory with world permission should not pass check, got {:?}", result_world); + Ok(()) +} \ No newline at end of file diff --git a/tests/test_format.rs b/tests/test_format.rs index a25ecc7..6c997d6 100644 --- a/tests/test_format.rs +++ b/tests/test_format.rs @@ -1,69 +1,63 @@ +mod test_lib; -#[cfg(test)] -mod test_utils; +use novops::modules::variables::VariableOutput; +use novops::{prepare_variable_outputs, format_variable_outputs}; +use test_lib::test_setup; -#[cfg(test)] -mod tests { - use novops::modules::variables::VariableOutput; - use novops::{prepare_variable_outputs, format_variable_outputs}; - use crate::test_utils::test_setup; - - #[tokio::test] - async fn test_prepare_variable_output() -> Result<(), anyhow::Error>{ - test_setup().await?; +#[tokio::test] +async fn test_prepare_variable_output() -> Result<(), anyhow::Error>{ + test_setup().await?; - let val1 = "VALUE1"; - let var1 = VariableOutput{ - name: String::from("VAR1"), - value: String::from(val1) - }; + let val1 = "VALUE1"; + let var1 = VariableOutput{ + name: String::from("VAR1"), + value: String::from(val1) + }; - // Special characters should be escaped - let val2=r#"special_char_'"'"'!?`$abc_#~%*µ€{}[]-°+@à^ç=\"#; - let var2 = VariableOutput{ - name: String::from("VAR2"), - value: String::from(val2) - }; + // Special characters should be escaped + let val2=r#"special_char_'"'"'!?`$abc_#~%*µ€{}[]-°+@à^ç=\"#; + let var2 = VariableOutput{ + name: String::from("VAR2"), + value: String::from(val2) + }; - let vars = Vec::from([var1, var2]); + let vars = Vec::from([var1, var2]); - let result = prepare_variable_outputs(&vars); + let result = prepare_variable_outputs(&vars); - assert_eq!(result[0].value, val1); - assert_eq!(result[1].value, "special_char_'\"'\"'\"'\"'\"'\"'\"'\"'!?`$abc_#~%*µ€{}[]-°+@à^ç=\\"); + assert_eq!(result[0].value, val1); + assert_eq!(result[1].value, "special_char_'\"'\"'\"'\"'\"'\"'\"'\"'!?`$abc_#~%*µ€{}[]-°+@à^ç=\\"); - Ok(()) - } + Ok(()) +} - #[tokio::test] - async fn test_format_variable_outputs() -> Result<(), anyhow::Error> { +#[tokio::test] +async fn test_format_variable_outputs() -> Result<(), anyhow::Error> { - let var1 = VariableOutput{ - name: String::from("VAR1"), - value: String::from("VALUE1") - }; + let var1 = VariableOutput{ + name: String::from("VAR1"), + value: String::from("VALUE1") + }; - let var2 = VariableOutput{ - name: String::from("VAR2"), - value: String::from("VALUE2") - }; + let var2 = VariableOutput{ + name: String::from("VAR2"), + value: String::from("VALUE2") + }; - let vars = Vec::from([var1, var2]); + let vars = Vec::from([var1, var2]); - // dotenv - let result_dotenv = format_variable_outputs("dotenv", &vars)?; - assert_eq!(result_dotenv, "VAR1='VALUE1'\nVAR2='VALUE2'\n"); + // dotenv + let result_dotenv = format_variable_outputs("dotenv", &vars)?; + assert_eq!(result_dotenv, "VAR1='VALUE1'\nVAR2='VALUE2'\n"); - // dotenv-export - let result_dotenv_export = format_variable_outputs("dotenv-export", &vars)?; - assert_eq!(result_dotenv_export, "export VAR1='VALUE1'\nexport VAR2='VALUE2'\n"); + // dotenv-export + let result_dotenv_export = format_variable_outputs("dotenv-export", &vars)?; + assert_eq!(result_dotenv_export, "export VAR1='VALUE1'\nexport VAR2='VALUE2'\n"); - // unknown format expect error - let result_unknown = format_variable_outputs("unknown-zzzz", &vars); - assert!(result_unknown.is_err()); - - Ok(()) - } + // unknown format expect error + let result_unknown = format_variable_outputs("unknown-zzzz", &vars); + assert!(result_unknown.is_err()); + Ok(()) } \ No newline at end of file diff --git a/tests/test_gcloud.rs b/tests/test_gcloud.rs index b99b463..fbfa1a0 100644 --- a/tests/test_gcloud.rs +++ b/tests/test_gcloud.rs @@ -1,26 +1,22 @@ -mod test_utils; +mod test_lib; +use test_lib::{test_setup, load_env_dryrun_for}; +use log::info; -#[cfg(test)] -mod tests { - use crate::test_utils; - use log::{info}; +#[tokio::test] +async fn test_gcloud_secretmanager() -> Result<(), anyhow::Error> { - #[tokio::test] - async fn test_gcloud_secretmanager() -> Result<(), anyhow::Error> { + test_setup().await?; - test_utils::test_setup().await?; + let expect = "RESULT:projects/398497848942/secrets/test-novops/versions/latest"; + let outputs = load_env_dryrun_for("gcloud_secretmanager", "dev").await?; - let expect = "RESULT:projects/398497848942/secrets/test-novops/versions/latest"; - let outputs = test_utils::load_env_dryrun_for("gcloud_secretmanager", "dev").await?; + info!("test_gcloud_secretmanager: Found variables: {:?}", outputs.variables); + info!("test_gcloud_secretmanager: Found files: {:?}", outputs.files); - info!("test_gcloud_secretmanager: Found variables: {:?}", outputs.variables); - info!("test_gcloud_secretmanager: Found files: {:?}", outputs.files); + assert_eq!(outputs.variables.get("SECRETMANAGER_VAR_STRING").unwrap().value, expect); + assert_eq!(outputs.files.get("/tmp/gcloud_SECRETMANAGER_VAR_FILE").unwrap().content, expect.as_bytes()); - assert_eq!(outputs.variables.get("SECRETMANAGER_VAR_STRING").unwrap().value, expect); - assert_eq!(outputs.files.get("/tmp/gcloud_SECRETMANAGER_VAR_FILE").unwrap().content, expect.as_bytes()); - - Ok(()) - } + Ok(()) } \ No newline at end of file diff --git a/tests/test_hvault.rs b/tests/test_hvault.rs index 590718b..6a71442 100644 --- a/tests/test_hvault.rs +++ b/tests/test_hvault.rs @@ -1,245 +1,241 @@ -mod test_utils; - -#[cfg(test)] -mod tests { - use anyhow::Context; - use std::{ fs, path::PathBuf }; - use vaultrs::client::{VaultClient, VaultClientSettingsBuilder}; - use vaultrs::{ - self, - kv2, - kv1, - aws, - api::aws::requests::{ - SetConfigurationRequest, CreateUpdateRoleRequest - } - }; - use log::info; - use std::{ collections::HashMap, thread, time }; - use crate::test_utils::{load_env_for, test_setup, aws_ensure_role_exists, self}; - use novops::modules::hashivault::{ - client::{load_vault_token, load_vault_address}, - config::HashivaultConfig - }; - use novops::core::{NovopsConfig, NovopsContext}; - - - #[tokio::test] - async fn test_hashivault_kv2() -> Result<(), anyhow::Error> { - test_setup().await?; - let client = hashivault_test_client(); - - // enable kv2 engine - let opts = HashMap::from([("version".to_string(), "2".to_string())]); - enable_engine(&client, "kv2", "kv", Some(opts)).await?; - - // sleep a few seconds as creating kv2 secret engine may take a few seconds - // vault may return a 400 in the meantime - thread::sleep(time::Duration::from_secs(3)); - - kv2::set( - &client, - "kv2", - "test_hashivault_kv2", - &HashMap::from([("novops_secret", "s3cret_kv2")]) - ).await.with_context(|| "Error when setting test secret for kv2")?; - - let outputs = load_env_for("hvault_kv2", "dev").await?; - - assert_eq!(outputs.variables.get("HASHIVAULT_KV_V2_TEST").unwrap().value, "s3cret_kv2"); - - Ok(()) +mod test_lib; + +use anyhow::Context; +use std::{ fs, path::PathBuf }; +use vaultrs::client::{VaultClient, VaultClientSettingsBuilder}; +use vaultrs::{ + self, + kv2, + kv1, + aws, + api::aws::requests::{ + SetConfigurationRequest, CreateUpdateRoleRequest } +}; +use log::info; +use std::{ collections::HashMap, thread, time }; +use test_lib::{load_env_for, test_setup, aws_ensure_role_exists, create_dummy_context}; +use novops::modules::hashivault::{ + client::{load_vault_token, load_vault_address}, + config::HashivaultConfig +}; +use novops::core::{NovopsConfig, NovopsContext}; + + +#[tokio::test] +async fn test_hashivault_kv2() -> Result<(), anyhow::Error> { + test_setup().await?; + let client = hashivault_test_client(); + + // enable kv2 engine + let opts = HashMap::from([("version".to_string(), "2".to_string())]); + enable_engine(&client, "kv2", "kv", Some(opts)).await?; + + // sleep a few seconds as creating kv2 secret engine may take a few seconds + // vault may return a 400 in the meantime + thread::sleep(time::Duration::from_secs(3)); + + kv2::set( + &client, + "kv2", + "test_hashivault_kv2", + &HashMap::from([("novops_secret", "s3cret_kv2")]) + ).await.with_context(|| "Error when setting test secret for kv2")?; + + let outputs = load_env_for("hvault_kv2", "dev").await?; + + assert_eq!(outputs.variables.get("HASHIVAULT_KV_V2_TEST").unwrap().value, "s3cret_kv2"); + + Ok(()) +} + +#[tokio::test] +async fn test_hashivault_kv1() -> Result<(), anyhow::Error> { + test_setup().await?; + + let client = hashivault_test_client(); + enable_engine(&client, "kv1", "generic", None).await?; - #[tokio::test] - async fn test_hashivault_kv1() -> Result<(), anyhow::Error> { - test_setup().await?; - - let client = hashivault_test_client(); - enable_engine(&client, "kv1", "generic", None).await?; - - kv1::set( - &client, - "kv1", - "test_hashivault_kv1", - &HashMap::from([("novops_secret", "s3cret_kv1")]) - ).await.with_context(|| "Error when setting test secret for kv1")?; + kv1::set( + &client, + "kv1", + "test_hashivault_kv1", + &HashMap::from([("novops_secret", "s3cret_kv1")]) + ).await.with_context(|| "Error when setting test secret for kv1")?; - let outputs = load_env_for("hvault_kv1", "dev").await?; + let outputs = load_env_for("hvault_kv1", "dev").await?; - assert_eq!(outputs.variables.get("HASHIVAULT_KV_V1_TEST").unwrap().value, "s3cret_kv1"); + assert_eq!(outputs.variables.get("HASHIVAULT_KV_V1_TEST").unwrap().value, "s3cret_kv1"); - Ok(()) - } + Ok(()) +} - #[tokio::test] - async fn test_hashivault_aws() -> Result<(), anyhow::Error> { - test_setup().await?; - - // Setup Vault AWS SE for Localstack and create Hashivault role - let client = hashivault_test_client(); - enable_engine(&client, "test_aws", "aws", None).await?; - - aws::config::set(&client, "test_aws", "test_key", "test_secret", Some(SetConfigurationRequest::builder() - .sts_endpoint("http://localstack:4566/") // Localstack URL reachable from Vault container in Docker Compose stack - .iam_endpoint("http://localstack:4566/") - )).await?; +#[tokio::test] +async fn test_hashivault_aws() -> Result<(), anyhow::Error> { + test_setup().await?; + + // Setup Vault AWS SE for Localstack and create Hashivault role + let client = hashivault_test_client(); + enable_engine(&client, "test_aws", "aws", None).await?; + + aws::config::set(&client, "test_aws", "test_key", "test_secret", Some(SetConfigurationRequest::builder() + .sts_endpoint("http://localstack:4566/") // Localstack URL reachable from Vault container in Docker Compose stack + .iam_endpoint("http://localstack:4566/") + )).await?; - aws::roles::create_update(&client, "test_aws", "test_role", "assumed_role", Some(CreateUpdateRoleRequest::builder() - .role_arns(vec!["arn:aws:iam::111122223333:role/test_role".to_string()]) - )).await?; + aws::roles::create_update(&client, "test_aws", "test_role", "assumed_role", Some(CreateUpdateRoleRequest::builder() + .role_arns(vec!["arn:aws:iam::111122223333:role/test_role".to_string()]) + )).await?; - // Make sure IAM Role exists on AWS side - aws_ensure_role_exists("test_role").await?; + // Make sure IAM Role exists on AWS side + aws_ensure_role_exists("test_role").await?; - // Generate credentials - let outputs = load_env_for("hvault_aws", "dev").await?; + // Generate credentials + let outputs = load_env_for("hvault_aws", "dev").await?; - info!("Hashivault AWS credentials: {:?}", outputs); + info!("Hashivault AWS credentials: {:?}", outputs); - assert!(outputs.variables.get("AWS_ACCESS_KEY_ID").unwrap().value.len() > 0); - assert!(outputs.variables.get("AWS_SECRET_ACCESS_KEY").unwrap().value.len() > 0); - assert!(outputs.variables.get("AWS_SESSION_TOKEN").unwrap().value.len() > 0); + assert!(outputs.variables.get("AWS_ACCESS_KEY_ID").unwrap().value.len() > 0); + assert!(outputs.variables.get("AWS_SECRET_ACCESS_KEY").unwrap().value.len() > 0); + assert!(outputs.variables.get("AWS_SESSION_TOKEN").unwrap().value.len() > 0); - Ok(()) - } + Ok(()) +} - /** - * Check vault token is loaded in various situations in the proper order - */ - #[tokio::test] - async fn test_hashivault_client_token_load() -> Result<(), anyhow::Error> { - test_setup().await?; - - // Empty config should yield empty token - let ctx_empty = create_dummy_context_with_hvault(None, None, None); - - let result_empty = load_vault_token(&ctx_empty, None, None)?; - assert!(result_empty.is_empty()); - - // Token in home should be used - // Create dummy token for testing - // use linefeed and empty space to also test trimming - let home_token = "hometoken\n \n"; - let expected_home_token = "hometoken"; - - let dummy_home_path = PathBuf::from("/tmp"); - let home_token_path = dummy_home_path.join(".vault-token"); - let home_var = Some(dummy_home_path); - fs::write(home_token_path, home_token).with_context(|| "Couldn't write test token in /tmp")?; - - let ctx_empty = create_dummy_context_with_hvault(None, None, None); - let result_home_token = load_vault_token(&ctx_empty, home_var.clone(), None)?; - - assert_eq!(result_home_token, expected_home_token); - - // Providing plain token should use it - let token_plain = "token_plain"; - let ctx_token_path = create_dummy_context_with_hvault( - None, Some(String::from(token_plain)), None); - let result_token_plain = load_vault_token(&ctx_token_path, home_var.clone(), None)?; - assert_eq!(result_token_plain, token_plain); - - // Providing token path should read token path before plain token - let tmp_token_path = "/tmp/token"; - let token_file_content = "token_in_file"; - fs::write(&tmp_token_path, token_file_content) - .with_context(|| format!("Couldn't write test token to {tmp_token_path}"))?; - - let ctx_token_path = create_dummy_context_with_hvault( - None, Some(String::from(token_plain)), Some(PathBuf::from(tmp_token_path))); - let result_token_path = load_vault_token(&ctx_token_path, home_var.clone(), None)?; - assert_eq!(result_token_path, token_file_content); - - // Providing token en var should use it before anything else - let env_var_token = String::from("envvartoken"); - let ctx_token_path = create_dummy_context_with_hvault( - None, Some(String::from(token_plain)), Some(PathBuf::from(tmp_token_path))); - let result_token_env_var = load_vault_token(&ctx_token_path, home_var.clone(), Some(env_var_token.clone()))?; - assert_eq!(result_token_env_var, env_var_token); - - Ok(()) - } +/** + * Check vault token is loaded in various situations in the proper order + */ +#[tokio::test] +async fn test_hashivault_client_token_load() -> Result<(), anyhow::Error> { + test_setup().await?; - /** - * Check vault address is loaded in various situations in the proper order - */ - #[tokio::test] - async fn test_hashivault_client_address_load() -> Result<(), anyhow::Error> { - test_setup().await?; - - // Empty config should yield empty address - let ctx_empty = create_dummy_context_with_hvault(None, None, None); - let result_empty = load_vault_address(&ctx_empty, None)?; - assert_eq!(result_empty, url::Url::parse("http://127.0.0.1:8200")?); - - // Vault address config should yield configured address - let addr_config = "https://dummy-vault-config"; - let ctx_addr = create_dummy_context_with_hvault( - Some(String::from(addr_config)), None, None); + // Empty config should yield empty token + let ctx_empty = create_dummy_context_with_hvault(None, None, None); - let result_config = load_vault_address(&ctx_addr, None)?; - assert_eq!(result_config, url::Url::parse(addr_config)?); - - // Vault address env var should be used first - let addr_var = String::from("https://env-var-address"); - let ctx_addr = create_dummy_context_with_hvault( - Some(String::from(addr_config)), None, None); + let result_empty = load_vault_token(&ctx_empty, None, None)?; + assert!(result_empty.is_empty()); + + // Token in home should be used + // Create dummy token for testing + // use linefeed and empty space to also test trimming + let home_token = "hometoken\n \n"; + let expected_home_token = "hometoken"; + + let dummy_home_path = PathBuf::from("/tmp"); + let home_token_path = dummy_home_path.join(".vault-token"); + let home_var = Some(dummy_home_path); + fs::write(home_token_path, home_token).with_context(|| "Couldn't write test token in /tmp")?; - let result_env_var = load_vault_address(&ctx_addr, Some(addr_var.clone()))?; - assert_eq!(result_env_var, url::Url::parse(&addr_var)?); - - Ok(()) - } + let ctx_empty = create_dummy_context_with_hvault(None, None, None); + let result_home_token = load_vault_token(&ctx_empty, home_var.clone(), None)?; + + assert_eq!(result_home_token, expected_home_token); + + // Providing plain token should use it + let token_plain = "token_plain"; + let ctx_token_path = create_dummy_context_with_hvault( + None, Some(String::from(token_plain)), None); + let result_token_plain = load_vault_token(&ctx_token_path, home_var.clone(), None)?; + assert_eq!(result_token_plain, token_plain); + + // Providing token path should read token path before plain token + let tmp_token_path = "/tmp/token"; + let token_file_content = "token_in_file"; + fs::write(&tmp_token_path, token_file_content) + .with_context(|| format!("Couldn't write test token to {tmp_token_path}"))?; + + let ctx_token_path = create_dummy_context_with_hvault( + None, Some(String::from(token_plain)), Some(PathBuf::from(tmp_token_path))); + let result_token_path = load_vault_token(&ctx_token_path, home_var.clone(), None)?; + assert_eq!(result_token_path, token_file_content); + + // Providing token en var should use it before anything else + let env_var_token = String::from("envvartoken"); + let ctx_token_path = create_dummy_context_with_hvault( + None, Some(String::from(token_plain)), Some(PathBuf::from(tmp_token_path))); + let result_token_env_var = load_vault_token(&ctx_token_path, home_var.clone(), Some(env_var_token.clone()))?; + assert_eq!(result_token_env_var, env_var_token); + + Ok(()) +} + +/** + * Check vault address is loaded in various situations in the proper order + */ +#[tokio::test] +async fn test_hashivault_client_address_load() -> Result<(), anyhow::Error> { + test_setup().await?; + + // Empty config should yield empty address + let ctx_empty = create_dummy_context_with_hvault(None, None, None); + let result_empty = load_vault_address(&ctx_empty, None)?; + assert_eq!(result_empty, url::Url::parse("http://127.0.0.1:8200")?); + + // Vault address config should yield configured address + let addr_config = "https://dummy-vault-config"; + let ctx_addr = create_dummy_context_with_hvault( + Some(String::from(addr_config)), None, None); + + let result_config = load_vault_address(&ctx_addr, None)?; + assert_eq!(result_config, url::Url::parse(addr_config)?); + + // Vault address env var should be used first + let addr_var = String::from("https://env-var-address"); + let ctx_addr = create_dummy_context_with_hvault( + Some(String::from(addr_config)), None, None); + + let result_env_var = load_vault_address(&ctx_addr, Some(addr_var.clone()))?; + assert_eq!(result_env_var, url::Url::parse(&addr_var)?); + + Ok(()) +} + +/** + * Test client used to prepare Hashivault with a few secrets + * Voluntarily separated from implemented client to make tests independent + */ +fn hashivault_test_client() -> VaultClient { + return VaultClient::new( + VaultClientSettingsBuilder::default() + .token("novops") + .build() + .unwrap() + ).unwrap(); +} + +async fn enable_engine(client: &VaultClient, path: &str, engine_type: &str, opts: Option>) -> Result<(), anyhow::Error> { + let mounts = vaultrs::sys::mount::list(client).await + .with_context(|| "Couldn't list secret engines")?; + + if ! mounts.contains_key(format!("{:}/", path).as_str()) { - /** - * Test client used to prepare Hashivault with a few secrets - * Voluntarily separated from implemented client to make tests independent - */ - fn hashivault_test_client() -> VaultClient { - return VaultClient::new( - VaultClientSettingsBuilder::default() - .token("novops") - .build() - .unwrap() - ).unwrap(); - } + let mut options = vaultrs::api::sys::requests::EnableEngineRequest::builder(); + if opts.is_some(){ + options.options(opts.unwrap()); + }; - async fn enable_engine(client: &VaultClient, path: &str, engine_type: &str, opts: Option>) -> Result<(), anyhow::Error> { - let mounts = vaultrs::sys::mount::list(client).await - .with_context(|| "Couldn't list secret engines")?; - - if ! mounts.contains_key(format!("{:}/", path).as_str()) { - - let mut options = vaultrs::api::sys::requests::EnableEngineRequest::builder(); - if opts.is_some(){ - options.options(opts.unwrap()); - }; - - vaultrs::sys::mount::enable(client, path, engine_type, Some(&mut options)).await - .with_context(|| format!("Couldn!'t enable engine {:} at path {:}", engine_type, path))?; - } else { - info!("Secret engine {:} already enabled at {:}", engine_type, path) - } - - Ok(()) + vaultrs::sys::mount::enable(client, path, engine_type, Some(&mut options)).await + .with_context(|| format!("Couldn!'t enable engine {:} at path {:}", engine_type, path))?; + } else { + info!("Secret engine {:} already enabled at {:}", engine_type, path) } + + Ok(()) +} - fn create_dummy_context_with_hvault(addr: Option, token: Option, token_path: Option) -> NovopsContext { - let mut ctx = test_utils::create_dummy_context(); - - let mut novops_config = NovopsConfig::default(); +fn create_dummy_context_with_hvault(addr: Option, token: Option, token_path: Option) -> NovopsContext { + let mut ctx = create_dummy_context(); - novops_config.hashivault = Some(HashivaultConfig { - address: addr, - token: token, - token_path: token_path, - verify: Some(false) - }); + let mut novops_config = NovopsConfig::default(); - ctx.config_file_data.config = Some(novops_config); + novops_config.hashivault = Some(HashivaultConfig { + address: addr, + token: token, + token_path: token_path, + verify: Some(false) + }); - return ctx - } + ctx.config_file_data.config = Some(novops_config); + return ctx } \ No newline at end of file diff --git a/tests/test_lib/mod.rs b/tests/test_lib/mod.rs new file mode 100644 index 0000000..9011b03 --- /dev/null +++ b/tests/test_lib/mod.rs @@ -0,0 +1,162 @@ +use log::debug; +use novops::core::{NovopsConfig, NovopsConfigDefault, NovopsConfigFile, NovopsContext}; +use novops::modules::aws::{client::get_iam_client, config::AwsClientConfig}; +use novops::{load_context_and_resolve, NovopsLoadArgs, NovopsOutputs}; +use std::collections::HashMap; +use std::env; +use std::fs; +use std::fs::Permissions; +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; + +#[allow(dead_code)] +pub const TEST_DIR: &str = "tests/output"; + +/** + * Create a temporary dir to be used for test + * Mostly used as Novops workdir named after test + */ +#[allow(dead_code)] +pub fn clean_and_setup_test_dir(test_name: &str) -> Result { + let test_output_dir = env::current_dir()?.join(TEST_DIR).join(test_name); + + if test_output_dir.exists() { + fs::remove_dir_all(&test_output_dir)?; + } + + fs::create_dir_all(&test_output_dir)?; + fs::set_permissions(&test_output_dir, Permissions::from_mode(0o700))?; + return Ok(test_output_dir); +} + +/** + * Load Novops environment for tests/.novops..yml + */ +#[allow(dead_code)] +pub async fn load_env_for(conf_name: &str, env: &str) -> Result { + _load_env_for(conf_name, env, false).await +} + +#[allow(dead_code)] +pub async fn load_env_dryrun_for( + conf_name: &str, + env: &str, +) -> Result { + _load_env_for(conf_name, env, true).await +} + +#[allow(dead_code)] +async fn _load_env_for( + conf_name: &str, + env: &str, + dry_run: bool, +) -> Result { + let args = NovopsLoadArgs { + config: format!("tests/.novops.{}.yml", conf_name), + env: Some(env.to_string()), + working_directory: None, + skip_working_directory_check: Some(false), + dry_run: Some(dry_run), + }; + + let outputs = load_context_and_resolve(&args).await?; + + return Ok(outputs); +} + +/** + * Perform test setup before running tests + * - Use common logging + * - Use test AWS config + */ +#[allow(dead_code)] +pub async fn test_setup() -> Result<(), anyhow::Error> { + // enable logger + match env_logger::try_init() { + Ok(_) => {} + Err(e) => { + debug!("env_logger::try_init() error: {:?}", e) + } + }; + + // use known AWS config + let aws_config = std::env::current_dir()?.join("tests/aws/config"); + let aws_creds = std::env::current_dir()?.join("tests/aws/credentials"); + + std::env::set_var("AWS_CONFIG_FILE", aws_config.to_str().unwrap()); + std::env::set_var("AWS_SHARED_CREDENTIALS_FILE", &aws_creds.to_str().unwrap()); + + // known age keys + std::env::set_var("SOPS_AGE_KEY_FILE", "tests/sops/age1"); + + Ok(()) +} + +#[allow(dead_code)] +pub fn aws_test_config() -> AwsClientConfig { + let mut aws_conf = AwsClientConfig::default(); + aws_conf.endpoint("http://localhost:4566/"); // Localstack + return aws_conf; +} + +/** + * create test IAM role to impersonate, delete it first if already exists + */ +#[allow(dead_code)] +pub async fn aws_ensure_role_exists(role_name: &str) -> Result<(), anyhow::Error> { + let client = get_iam_client(&aws_test_config()).await?; + let existing_role_result = client.get_role().role_name(role_name).send().await; + + match existing_role_result { + Ok(_) => { + // role exists, clean before running test + client.delete_role().role_name(role_name).send().await?; + } + Err(_) => {} // role do not exists, do nothing + } + + client + .create_role() + .role_name(role_name) + .assume_role_policy_document( + r#"{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": "111122223333" + }, + "Action": "sts:AssumeRole" + } + ] + }"#, + ) + .send() + .await + .expect("Valid create role response"); + + Ok(()) +} + +#[allow(dead_code)] +pub fn create_dummy_context() -> NovopsContext { + NovopsContext { + env_name: String::from("dev"), + app_name: String::from("test-empty"), + workdir: PathBuf::from("/tmp"), + config_file_data: NovopsConfigFile { + name: Some(String::from("test-empty")), + environments: HashMap::new(), + config: Some(NovopsConfig { + default: Some(NovopsConfigDefault { + environment: Some(String::from("dev")), + }), + hashivault: None, + aws: None, + }), + }, + env_var_filepath: PathBuf::from("/tmp/vars"), + dry_run: false, + } +} diff --git a/tests/test_schema.rs b/tests/test_schema.rs index e4d17a4..4c92ed4 100644 --- a/tests/test_schema.rs +++ b/tests/test_schema.rs @@ -1,25 +1,21 @@ -#[cfg(test)] -mod test_utils; +mod test_lib; -#[cfg(test)] -mod tests { - use novops::get_config_schema; - use crate::test_utils::test_setup; - use std::fs; - use pretty_assertions::assert_eq; +use novops::get_config_schema; +use test_lib::test_setup; +use std::fs; +use pretty_assertions::assert_eq; - #[tokio::test] - async fn test_load_simple_config() -> Result<(), anyhow::Error>{ - test_setup().await?; +#[tokio::test] +async fn test_load_simple_config() -> Result<(), anyhow::Error>{ + test_setup().await?; - let schema_path = "docs/schema/config-schema.json"; - let schema = get_config_schema()?; - let expected = fs::read_to_string(schema_path)?.trim().to_string(); + let schema_path = "docs/schema/config-schema.json"; + let schema = get_config_schema()?; + let expected = fs::read_to_string(schema_path)?.trim().to_string(); - assert_eq!(schema, expected, "Generated schema and {} do not match. Did you run 'make doc' and commit changes?", schema_path); + assert_eq!(schema, expected, "Generated schema and {} do not match. Did you run 'make doc' and commit changes?", schema_path); - Ok(()) - } + Ok(()) +} -} \ No newline at end of file diff --git a/tests/test_sops.rs b/tests/test_sops.rs index 3b80f65..6b4162b 100644 --- a/tests/test_sops.rs +++ b/tests/test_sops.rs @@ -1,39 +1,33 @@ -mod test_utils; +pub mod test_lib; +#[tokio::test] +async fn test_sops_value() -> Result<(), anyhow::Error> { -#[cfg(test)] -mod tests { - use crate::test_utils; + test_lib::test_setup().await?; - #[tokio::test] - async fn test_sops_value() -> Result<(), anyhow::Error> { + let expected_value = "nestedValue"; + let expected_file_content = "nested:\n data:\n nestedKey: nestedValue\nanother_value: foo\n"; - test_utils::test_setup().await?; + let outputs = test_lib::load_env_for("sops", "dev").await?; + + assert_eq!(outputs.variables.get("SOPS_VALUE").unwrap().value, expected_value); + assert_eq!(outputs.files.get("/tmp/SOPS_FILE").unwrap().content.clone(), expected_file_content.as_bytes()); - let expected_value = "nestedValue"; - let expected_file_content = "nested:\n data:\n nestedKey: nestedValue\nanother_value: foo\n"; + Ok(()) +} - let outputs = test_utils::load_env_for("sops", "dev").await?; - - assert_eq!(outputs.variables.get("SOPS_VALUE").unwrap().value, expected_value); - assert_eq!(outputs.files.get("/tmp/SOPS_FILE").unwrap().content.clone(), expected_file_content.as_bytes()); +#[tokio::test] +async fn test_sops_dotenv() -> Result<(), anyhow::Error> { - Ok(()) - } + test_lib::test_setup().await?; - #[tokio::test] - async fn test_sops_dotenv() -> Result<(), anyhow::Error> { + let outputs = test_lib::load_env_for("sops", "integ").await?; + + assert_eq!(outputs.variables.get("APP_TOKEN").unwrap().value, "s3cret!"); + assert_eq!(outputs.variables.get("app_host").unwrap().value, "http://localhost"); + assert_eq!(outputs.variables.get("WITH_LINES").unwrap().value, "foo\\nbar\\nbaz\\\\n\\nzzz\\n"); + assert_eq!(outputs.variables.get("WITH_EQUAL").unwrap().value, "EQUAL_CHAR=EQUAL_VALUE"); + assert_eq!(outputs.variables.get("nestedKey").unwrap().value, "nestedValue"); - test_utils::test_setup().await?; - - let outputs = test_utils::load_env_for("sops", "integ").await?; - - assert_eq!(outputs.variables.get("APP_TOKEN").unwrap().value, "s3cret!"); - assert_eq!(outputs.variables.get("app_host").unwrap().value, "http://localhost"); - assert_eq!(outputs.variables.get("WITH_LINES").unwrap().value, "foo\\nbar\\nbaz\\\\n\\nzzz\\n"); - assert_eq!(outputs.variables.get("WITH_EQUAL").unwrap().value, "EQUAL_CHAR=EQUAL_VALUE"); - assert_eq!(outputs.variables.get("nestedKey").unwrap().value, "nestedValue"); - - Ok(()) - } + Ok(()) } \ No newline at end of file diff --git a/tests/test_utils.rs b/tests/test_utils.rs deleted file mode 100644 index fcdf0c4..0000000 --- a/tests/test_utils.rs +++ /dev/null @@ -1,151 +0,0 @@ -use std::path::PathBuf; -use std::fs; -use std::fs::Permissions; -use std::os::unix::fs::PermissionsExt; -use std::env; -use novops::{NovopsOutputs, NovopsLoadArgs, load_context_and_resolve}; -use novops::core::{NovopsContext, NovopsConfig, NovopsConfigFile, NovopsConfigDefault}; -use std::collections::HashMap; -use log::debug; -use novops::modules::aws::{client::get_iam_client, config::AwsClientConfig}; - -pub const TEST_DIR: &str = "tests/output"; - -/** - * Create a temporary dir to be used for test - * Mostly used as Novops workdir named after test - */ -#[cfg(test)] -#[allow(dead_code)] -pub fn clean_and_setup_test_dir(test_name: &str) -> Result { - let test_output_dir = env::current_dir()?.join(TEST_DIR).join(test_name); - - if test_output_dir.exists(){ - fs::remove_dir_all(&test_output_dir)?; - } - - fs::create_dir_all(&test_output_dir)?; - fs::set_permissions(&test_output_dir, Permissions::from_mode(0o700))?; - return Ok(test_output_dir) -} - -/** - * Load Novops environment for tests/.novops..yml - */ -#[cfg(test)] -#[allow(dead_code)] -pub async fn load_env_for(conf_name: &str, env: &str) -> Result { - _load_env_for(conf_name, env, false).await -} - -#[cfg(test)] -#[allow(dead_code)] -pub async fn load_env_dryrun_for(conf_name: &str, env: &str) -> Result { - _load_env_for(conf_name, env, true).await -} - -async fn _load_env_for(conf_name: &str, env: &str, dry_run: bool) -> Result { - let args = NovopsLoadArgs { - config: format!("tests/.novops.{}.yml", conf_name), - env: Some(env.to_string()), - working_directory: None, - skip_working_directory_check: Some(false), - dry_run: Some(dry_run) - }; - - let outputs = load_context_and_resolve(&args).await?; - - return Ok(outputs); -} - -/** - * Perform test setup before running tests - * - Use common logging - * - Use test AWS config - */ -pub async fn test_setup() -> Result<(), anyhow::Error>{ - - // enable logger - match env_logger::try_init() { - Ok(_) => {}, - Err(e) => {debug!("env_logger::try_init() error: {:?}", e)}, - }; - - // use known AWS config - let aws_config = std::env::current_dir()?.join("tests/aws/config"); - let aws_creds = std::env::current_dir()?.join("tests/aws/credentials"); - - std::env::set_var("AWS_CONFIG_FILE", aws_config.to_str().unwrap()); - std::env::set_var("AWS_SHARED_CREDENTIALS_FILE", &aws_creds.to_str().unwrap()); - - // known age keys - std::env::set_var("SOPS_AGE_KEY_FILE", "tests/sops/age1"); - - Ok(()) -} - -#[cfg(test)] -#[allow(dead_code)] -pub fn aws_test_config() -> AwsClientConfig{ - let mut aws_conf = AwsClientConfig::default(); - aws_conf.endpoint("http://localhost:4566/"); // Localstack - return aws_conf; -} - -/** - * create test IAM role to impersonate, delete it first if already exists - */ -#[cfg(test)] -#[allow(dead_code)] -pub async fn aws_ensure_role_exists(role_name: &str) -> Result<(), anyhow::Error> { - let client = get_iam_client(&aws_test_config()).await?; - let existing_role_result = client.get_role().role_name(role_name).send().await; - - match existing_role_result { - Ok(_) => { // role exists, clean before running test - client.delete_role().role_name(role_name).send().await?; - } - Err(_) => {}, // role do not exists, do nothing - } - - client.create_role() - .role_name(role_name) - .assume_role_policy_document(r#"{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "AWS": "111122223333" - }, - "Action": "sts:AssumeRole" - } - ] - }"#) - .send().await.expect("Valid create role response"); - - Ok(()) -} - -#[cfg(test)] -#[allow(dead_code)] -pub fn create_dummy_context() -> NovopsContext{ - NovopsContext { - env_name: String::from("dev"), - app_name: String::from("test-empty"), - workdir: PathBuf::from("/tmp"), - config_file_data: NovopsConfigFile{ - name: Some(String::from("test-empty")), - environments: HashMap::new(), - config: Some(NovopsConfig { - default: Some(NovopsConfigDefault { - environment: Some(String::from("dev")) - }), - hashivault: None, - aws: None - }) - }, - env_var_filepath: PathBuf::from("/tmp/vars"), - dry_run: false - } -} \ No newline at end of file