diff --git a/Cargo.toml b/Cargo.toml index 68bfba33..d5ca61b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,4 @@ [workspace] +resolver = "2" -members = ["dotenv", "dotenv_codegen"] +members = ["dotenv", "dotenv_codegen", "test_util"] diff --git a/dotenv/Cargo.toml b/dotenv/Cargo.toml index 51b32a3e..84c41eb8 100644 --- a/dotenv/Cargo.toml +++ b/dotenv/Cargo.toml @@ -29,8 +29,8 @@ required-features = ["cli"] clap = { version = "4.3.11", optional = true } [dev-dependencies] +dotenvy_test_util = { path = "../test_util", version = "0.1.0" } tempfile = "3.3.0" -once_cell = "1.16.0" [features] cli = ["clap"] diff --git a/dotenv/tests/integration/api/dotenv.rs b/dotenv/tests/integration/api/dotenv.rs new file mode 100644 index 00000000..34966cdb --- /dev/null +++ b/dotenv/tests/integration/api/dotenv.rs @@ -0,0 +1,36 @@ +use crate::util::*; +use dotenvy::dotenv; +use dotenvy_test_util::*; + +#[test] +fn default_env_ok() { + test_in_default_env(|| { + dotenv().ok(); + assert_default_keys(); + }); +} + +#[test] +fn default_env_unwrap() { + test_in_default_env(|| { + dotenv().unwrap(); + assert_default_keys(); + }); +} + +#[test] +fn default_env_unwrap_path() { + let testenv = TestEnv::default(); + test_default_envfile_path(&testenv); +} + +#[test] +fn explicit_no_override() { + let mut testenv = TestEnv::init(); + testenv.add_env_var("FOOO", "bar"); + testenv.add_envfile(".env", "FOOO=notbar"); + test_in_env(&testenv, || { + dotenv().unwrap(); + assert_env_var("FOOO", "bar"); + }) +} diff --git a/dotenv/tests/integration/api/dotenv_iter.rs b/dotenv/tests/integration/api/dotenv_iter.rs new file mode 100644 index 00000000..c0c6ea9b --- /dev/null +++ b/dotenv/tests/integration/api/dotenv_iter.rs @@ -0,0 +1,103 @@ +use crate::util::*; +use dotenvy::dotenv_iter; +use dotenvy_test_util::*; + +#[test] +fn default_env_ok() { + test_in_default_env(|| { + dotenv_iter().ok(); + assert_default_existing_var(); + // the envfile shouldn't be loaded into the environment + assert_env_var_unset(DEFAULT_TEST_KEY); + }); +} + +#[test] +fn default_env_unwrap() { + test_in_default_env(|| { + dotenv_iter().unwrap(); + assert_default_existing_var(); + assert_env_var_unset(DEFAULT_TEST_KEY); + }); +} + +#[test] +fn no_envfile_ok() { + let testenv = TestEnv::init(); + test_in_env(&testenv, || { + dotenv_iter().ok(); + assert_default_keys_unset(); + }); +} + +#[test] +fn no_envfile_err() { + let testenv = TestEnv::init(); + test_in_env(&testenv, || match dotenv_iter() { + Ok(_) => panic!("should have failed"), + Err(err) => assert_err_not_found(err), + }); +} + +#[test] +fn no_vars() { + let testenv = TestEnv::init_with_envfile(""); + test_in_env(&testenv, || { + dotenv_iter().unwrap().for_each(|_| { + panic!("should have no keys"); + }); + }); +} + +#[test] +fn one_var() { + let testenv = TestEnv::init_with_envfile("FOOO=bar"); + test_in_env(&testenv, || { + let (key, value) = dotenv_iter_unwrap_one_item(); + assert_eq!(key, "FOOO"); + assert_eq!(value, "bar"); + }); +} + +#[test] +fn one_var_only() { + let testenv = TestEnv::init_with_envfile("FOOO=bar"); + test_in_env(&testenv, || { + let count = dotenv_iter().expect("valid file").count(); + assert_eq!(1, count); + }); +} + +#[test] +fn one_var_empty() { + let testenv = TestEnv::init_with_envfile("FOOO="); + test_in_env(&testenv, || { + let (key, value) = dotenv_iter_unwrap_one_item(); + assert_eq!(key, "FOOO"); + assert_eq!(value, ""); + }); +} + +#[test] +fn two_vars_into_hash_map() { + check_iter_default_envfile_into_hash_map(dotenv_iter); +} + +#[test] +fn explicit_no_override() { + let mut testenv = TestEnv::init(); + testenv.add_env_var("FOOO", "bar"); + testenv.add_envfile(".env", "FOOO=notbar"); + test_in_env(&testenv, || { + dotenv_iter().unwrap(); + assert_env_var("FOOO", "bar"); + }) +} + +fn dotenv_iter_unwrap_one_item() -> (String, String) { + dotenv_iter() + .expect("valid file") + .next() + .expect("one item") + .expect("valid item") +} diff --git a/dotenv/tests/integration/api/dotenv_override.rs b/dotenv/tests/integration/api/dotenv_override.rs new file mode 100644 index 00000000..b7a514f7 --- /dev/null +++ b/dotenv/tests/integration/api/dotenv_override.rs @@ -0,0 +1,83 @@ +use crate::util::*; +use dotenvy::dotenv_override; +use dotenvy_test_util::*; + +#[test] +fn no_file_ok() { + let testenv = TestEnv::init(); + test_in_env(&testenv, || { + dotenv_override().ok(); + }); +} + +#[test] +fn no_file_err() { + let testenv = TestEnv::init(); + test_in_env(&testenv, || { + let err = dotenv_override().unwrap_err(); + assert_err_not_found(err); + }); +} + +#[test] +fn empty_file_is_ok() { + let testenv = TestEnv::init_with_envfile(""); + test_in_env(&testenv, || { + assert!(dotenv_override().is_ok()); + }); +} + +#[test] +fn one_new_var() { + let testenv = TestEnv::init_with_envfile("FOOO=bar"); + test_in_env(&testenv, || { + dotenv_override().unwrap(); + assert_env_var("FOOO", "bar"); + }); +} + +#[test] +fn one_old_var() { + let mut testenv = TestEnv::init_with_envfile("FOOO=from_file"); + testenv.add_env_var("FOOO", "from_env"); + test_in_env(&testenv, || { + assert_env_var("FOOO", "from_env"); + dotenv_override().unwrap(); + assert_env_var("FOOO", "from_file"); + }); +} + +#[test] +fn one_old_var_one_new_var() { + let vars = [("FOOO", "from_file"), ("BARR", "new")]; + let envfile = create_custom_envfile(&vars); + let mut testenv = TestEnv::init_with_envfile(envfile); + testenv.add_env_var("FOOO", "from_env"); + test_in_env(&testenv, || { + assert_env_var_unset("BARR"); + dotenv_override().unwrap(); + assert_env_vars(&vars); + }); +} + +#[test] +fn substitute_self() { + let mut testenv = TestEnv::init_with_envfile("FOOO=$FOOO+1"); + testenv.add_env_var("FOOO", "test"); + test_in_env(&testenv, || { + assert_env_var("FOOO", "test"); + dotenv_override().unwrap(); + assert_env_var("FOOO", "test+1"); + }); +} + +#[test] +fn substitute_self_twice() { + let mut testenv = TestEnv::init_with_envfile("FOOO=$FOOO+1\nFOOO=$FOOO+1"); + testenv.add_env_var("FOOO", "test"); + test_in_env(&testenv, || { + assert_env_var("FOOO", "test"); + dotenv_override().unwrap(); + assert_env_var("FOOO", "test+1+1"); + }); +} diff --git a/dotenv/tests/integration/api/from_filename.rs b/dotenv/tests/integration/api/from_filename.rs new file mode 100644 index 00000000..d5f82c82 --- /dev/null +++ b/dotenv/tests/integration/api/from_filename.rs @@ -0,0 +1,117 @@ +use crate::util::*; +use dotenvy::from_filename; +use dotenvy_test_util::*; + +#[test] +fn no_file() { + let testenv = TestEnv::init(); + test_in_env(&testenv, || { + let err = from_filename("nonexistent.env").unwrap_err(); + assert_err_not_found(err); + }); +} + +#[test] +fn empty_default_file() { + let testenv = TestEnv::init_with_envfile(""); + test_in_env(&testenv, || { + assert!(from_filename(".env").is_ok()); + }); +} + +#[test] +fn empty_custom_file() { + let mut testenv = TestEnv::init(); + testenv.add_envfile(".custom.env", ""); + test_in_env(&testenv, || { + assert!(from_filename(".custom.env").is_ok()); + }); +} + +#[test] +fn return_path_valid() { + let testenv = TestEnv::default(); + test_in_env(&testenv, || { + let actual = from_filename(".env").unwrap(); + assert_default_envfile_path(&testenv, &actual); + }); +} + +#[test] +fn default_file_not_read_on_missing_file() { + test_in_default_env(|| { + let err = from_filename("nonexistent.env").unwrap_err(); + assert_err_not_found(err); + assert_env_var_unset(DEFAULT_TEST_KEY); + }) +} + +#[test] +fn dotenv_then_custom() { + let mut testenv = TestEnv::default(); + testenv.add_envfile("custom", KEYVAL_1); + test_in_env(&testenv, || { + dotenvy::dotenv().unwrap(); + from_filename("custom").unwrap(); + assert_env_var(KEY_1, VAL_1); + assert_default_keys(); + }); +} + +#[test] +fn dotenv_then_custom_no_override() { + let mut testenv = TestEnv::default(); + testenv.add_envfile("custom", format!("{DEFAULT_TEST_KEY}=from_custom")); + test_in_env(&testenv, || { + dotenvy::dotenv().unwrap(); + from_filename("custom").unwrap(); + assert_default_keys(); + }); +} + +#[test] +fn explicit_no_override() { + let mut testenv = TestEnv::init(); + testenv.add_env_var(KEY_1, VAL_1); + testenv.add_envfile("custom", format!("{KEY_1}=from_custom")); + test_in_env(&testenv, || { + from_filename("custom").unwrap(); + assert_env_var(KEY_1, VAL_1); + }); +} + +#[test] +fn child_dir() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("child"); + testenv.add_envfile("child/custom", KEYVAL_1); + test_in_env(&testenv, || { + from_filename("child/custom").unwrap(); + assert_env_var(KEY_1, VAL_1); + }); +} + +#[test] +fn parent_dir_relative_path() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("child"); + testenv.add_envfile("custom.env", KEYVAL_1); + testenv.set_work_dir("child"); + test_in_env(&testenv, || { + from_filename("../custom.env").unwrap(); + assert_env_var(KEY_1, VAL_1); + }); +} + +#[test] +fn parent_dir_absolute_path() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("child"); + testenv.add_envfile("custom.env", KEYVAL_1); + testenv.set_work_dir("child"); + test_in_env(&testenv, || { + let path = canonicalize_envfile_path(&testenv, "custom.env"); + from_filename(path).unwrap(); + assert_env_var(KEY_1, VAL_1); + }); +} diff --git a/dotenv/tests/integration/api/from_filename_iter.rs b/dotenv/tests/integration/api/from_filename_iter.rs new file mode 100644 index 00000000..c9f452e6 --- /dev/null +++ b/dotenv/tests/integration/api/from_filename_iter.rs @@ -0,0 +1,103 @@ +use std::path::Path; + +use crate::util::*; +use dotenvy::from_filename_iter; +use dotenvy_test_util::*; + +#[test] +fn no_file_ok() { + let testenv = TestEnv::init(); + test_in_env(&testenv, || { + from_filename_iter("nonexistent").ok(); + }); +} + +#[test] +fn no_file_err() { + let testenv = TestEnv::init(); + test_in_env(&testenv, || match from_filename_iter("nonexistent") { + Ok(_) => panic!("expected error"), + Err(err) => assert_err_not_found(err), + }); +} + +#[test] +fn empty_default_file() { + let testenv = TestEnv::init_with_envfile(""); + test_in_env(&testenv, || { + from_filename_iter(".env").unwrap().for_each(|_| { + panic!("should have no keys"); + }); + }); +} + +#[test] +fn empty_custom_file() { + let mut testenv = TestEnv::init(); + testenv.add_envfile(".custom.env", ""); + test_in_env(&testenv, || { + from_filename_iter(".custom.env").unwrap().for_each(|_| { + panic!("should have no keys"); + }); + }); +} + +#[test] +fn default_file_not_read_on_missing_file() { + test_in_default_env(|| { + from_filename_iter("nonexistent.env").ok(); + assert_env_var_unset(DEFAULT_TEST_KEY); + }) +} + +#[test] +fn child_dir() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("child"); + testenv.add_envfile("child/custom", KEYVAL_1); + test_in_env(&testenv, || { + assert_single_key_file("child/custom"); + }); +} + +#[test] +fn parent_dir_relative_path() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("child"); + testenv.add_envfile("custom.env", KEYVAL_1); + testenv.set_work_dir("child"); + test_in_env(&testenv, || { + assert_single_key_file("../custom.env"); + }); +} + +#[test] +fn parent_dir_absolute_path() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("child"); + testenv.add_envfile("custom.env", KEYVAL_1); + testenv.set_work_dir("child"); + test_in_env(&testenv, || { + let path = canonicalize_envfile_path(&testenv, "custom.env"); + assert_single_key_file(path); + }); +} + +#[test] +fn two_vars_into_hash_map() { + check_iter_default_envfile_into_hash_map(|| from_filename_iter(".env")); +} + +fn assert_single_key_file(path: impl AsRef) { + let (key, value) = from_filename_iter_unwrap_one_item(path); + assert_eq!(key, KEY_1); + assert_eq!(value, VAL_1); +} + +fn from_filename_iter_unwrap_one_item(path: impl AsRef) -> (String, String) { + from_filename_iter(path) + .expect("valid file") + .next() + .expect("one item") + .expect("valid item") +} diff --git a/dotenv/tests/integration/api/from_filename_override.rs b/dotenv/tests/integration/api/from_filename_override.rs new file mode 100644 index 00000000..b1bf54c8 --- /dev/null +++ b/dotenv/tests/integration/api/from_filename_override.rs @@ -0,0 +1,103 @@ +use crate::util::*; +use dotenvy::from_filename_override; +use dotenvy_test_util::*; + +#[test] +fn no_file_ok() { + let testenv = TestEnv::init(); + test_in_env(&testenv, || { + from_filename_override("nonexistent").ok(); + }); +} + +#[test] +fn no_file_err() { + let testenv = TestEnv::init(); + test_in_env(&testenv, || { + let err = from_filename_override("nonexistent.env").unwrap_err(); + assert_err_not_found(err); + }); +} + +#[test] +fn empty_default_file() { + let testenv = TestEnv::init_with_envfile(""); + test_in_env(&testenv, || { + assert!(from_filename_override(".env").is_ok()); + }); +} + +#[test] +fn empty_custom_file() { + let mut testenv = TestEnv::init(); + testenv.add_envfile(".custom.env", ""); + test_in_env(&testenv, || { + assert!(from_filename_override(".custom.env").is_ok()); + }); +} + +#[test] +fn return_path_valid() { + let testenv = TestEnv::default(); + test_in_env(&testenv, || { + let actual = from_filename_override(".env").unwrap(); + assert_default_envfile_path(&testenv, &actual); + }); +} + +#[test] +fn one_var_custom_file() { + let mut testenv = TestEnv::init(); + testenv.add_envfile(".custom.env", KEYVAL_1); + test_in_env(&testenv, || { + assert!(from_filename_override(".custom.env").is_ok()); + assert_env_var(KEY_1, VAL_1); + }); +} + +#[test] +fn override_existing_var_custom_file() { + let mut testenv = TestEnv::init(); + testenv.add_env_var("FOOO", "from_env"); + testenv.add_envfile(".custom.env", "FOOO=from_file"); + test_in_env(&testenv, || { + assert_env_var("FOOO", "from_env"); + from_filename_override(".custom.env").unwrap(); + assert_env_var("FOOO", "from_file"); + }); +} + +#[test] +fn default_override() { + test_in_default_env(|| { + from_filename_override(".env").unwrap(); + assert_env_var(DEFAULT_TEST_KEY, DEFAULT_TEST_VALUE); + assert_env_var(DEFAULT_EXISTING_KEY, DEFAULT_OVERRIDING_VALUE); + }) +} + +#[test] +fn substitute_self() { + let mut testenv = TestEnv::init(); + testenv.add_env_var("FOOO", "test"); + testenv.add_envfile(".custom.env", "FOOO=$FOOO+1"); + test_in_env(&testenv, || { + assert_env_var("FOOO", "test"); + from_filename_override(".custom.env").unwrap(); + assert_env_var("FOOO", "test+1"); + }); +} + +#[test] +fn substitute_self_two_files() { + let mut testenv = TestEnv::init(); + testenv.add_env_var("FOOO", "test"); + testenv.add_envfile(".custom1.env", "FOOO=$FOOO+1"); + testenv.add_envfile(".custom2.env", "FOOO=$FOOO+1"); + test_in_env(&testenv, || { + assert_env_var("FOOO", "test"); + from_filename_override(".custom1.env").unwrap(); + from_filename_override(".custom2.env").unwrap(); + assert_env_var("FOOO", "test+1+1"); + }); +} diff --git a/dotenv/tests/integration/api/from_path.rs b/dotenv/tests/integration/api/from_path.rs new file mode 100644 index 00000000..3159eeba --- /dev/null +++ b/dotenv/tests/integration/api/from_path.rs @@ -0,0 +1,108 @@ +use crate::util::*; +use dotenvy::from_path; +use dotenvy_test_util::*; + +#[test] +fn no_file() { + let testenv = TestEnv::init(); + test_in_env(&testenv, || { + let err = from_path("nonexistent.env").unwrap_err(); + assert_err_not_found(err); + }); +} + +#[test] +fn empty_default_file() { + let testenv = TestEnv::init_with_envfile(""); + test_in_env(&testenv, || { + assert!(from_path(".env").is_ok()); + }); +} + +#[test] +fn empty_custom_file() { + let mut testenv = TestEnv::init(); + testenv.add_envfile(".custom.env", ""); + test_in_env(&testenv, || { + assert!(from_path(".custom.env").is_ok()); + }); +} + +#[test] +fn default_file_not_read_on_missing_file() { + test_in_default_env(|| { + let err = from_path("nonexistent.env").unwrap_err(); + assert_err_not_found(err); + assert_env_var_unset(DEFAULT_TEST_KEY); + }) +} + +#[test] +fn dotenv_then_custom() { + let mut testenv = TestEnv::default(); + testenv.add_envfile("custom", KEYVAL_1); + test_in_env(&testenv, || { + dotenvy::dotenv().unwrap(); + from_path("custom").unwrap(); + assert_env_var(KEY_1, VAL_1); + assert_default_keys(); + }); +} + +#[test] +fn dotenv_then_custom_no_override() { + let mut testenv = TestEnv::default(); + testenv.add_envfile("custom", format!("{DEFAULT_TEST_KEY}=from_custom")); + test_in_env(&testenv, || { + dotenvy::dotenv().unwrap(); + from_path("custom").unwrap(); + assert_default_keys(); + }); +} + +#[test] +fn explicit_no_override() { + let mut testenv = TestEnv::init(); + testenv.add_env_var(KEY_1, VAL_1); + testenv.add_envfile("custom", format!("{KEY_1}=from_custom")); + test_in_env(&testenv, || { + from_path("custom").unwrap(); + assert_env_var(KEY_1, VAL_1); + }); +} + +#[test] +fn child_dir() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("child"); + testenv.add_envfile("child/custom", KEYVAL_1); + test_in_env(&testenv, || { + from_path("child/custom").unwrap(); + assert_env_var(KEY_1, VAL_1); + }); +} + +#[test] +fn parent_dir_relative_path() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("child"); + testenv.add_envfile("custom.env", KEYVAL_1); + testenv.set_work_dir("child"); + test_in_env(&testenv, || { + from_path("../custom.env").unwrap(); + assert_env_var(KEY_1, VAL_1); + }); +} + +#[test] +fn parent_dir_absolute_path() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("child"); + testenv.add_envfile("custom.env", KEYVAL_1); + testenv.set_work_dir("child"); + test_in_env(&testenv, || { + let path = canonicalize_envfile_path(&testenv, "custom.env"); + from_path(path).unwrap(); + assert_env_var(KEY_1, VAL_1); + }); +} diff --git a/dotenv/tests/integration/api/from_path_iter.rs b/dotenv/tests/integration/api/from_path_iter.rs new file mode 100644 index 00000000..1bca5958 --- /dev/null +++ b/dotenv/tests/integration/api/from_path_iter.rs @@ -0,0 +1,103 @@ +use std::path::Path; + +use crate::util::*; +use dotenvy::from_path_iter; +use dotenvy_test_util::*; + +#[test] +fn no_file_ok() { + let testenv = TestEnv::init(); + test_in_env(&testenv, || { + from_path_iter("nonexistent").ok(); + }); +} + +#[test] +fn no_file_err() { + let testenv = TestEnv::init(); + test_in_env(&testenv, || match from_path_iter("nonexistent") { + Ok(_) => panic!("expected error"), + Err(err) => assert_err_not_found(err), + }); +} + +#[test] +fn empty_default_file() { + let testenv = TestEnv::init_with_envfile(""); + test_in_env(&testenv, || { + from_path_iter(".env").unwrap().for_each(|_| { + panic!("should have no keys"); + }); + }); +} + +#[test] +fn empty_custom_file() { + let mut testenv = TestEnv::init(); + testenv.add_envfile(".custom.env", ""); + test_in_env(&testenv, || { + from_path_iter(".custom.env").unwrap().for_each(|_| { + panic!("should have no keys"); + }); + }); +} + +#[test] +fn default_file_not_read_on_missing_file() { + test_in_default_env(|| { + from_path_iter("nonexistent.env").ok(); + assert_env_var_unset(DEFAULT_TEST_KEY); + }) +} + +#[test] +fn child_dir() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("child"); + testenv.add_envfile("child/custom", KEYVAL_1); + test_in_env(&testenv, || { + assert_single_key_file("child/custom"); + }); +} + +#[test] +fn parent_dir_relative_path() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("child"); + testenv.add_envfile("custom.env", KEYVAL_1); + testenv.set_work_dir("child"); + test_in_env(&testenv, || { + assert_single_key_file("../custom.env"); + }); +} + +#[test] +fn parent_dir_absolute_path() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("child"); + testenv.add_envfile("custom.env", KEYVAL_1); + testenv.set_work_dir("child"); + test_in_env(&testenv, || { + let path = canonicalize_envfile_path(&testenv, "custom.env"); + assert_single_key_file(path); + }); +} + +#[test] +fn two_vars_into_hash_map() { + check_iter_default_envfile_into_hash_map(|| from_path_iter(".env")); +} + +fn assert_single_key_file(path: impl AsRef) { + let (key, value) = from_path_iter_unwrap_one_item(path); + assert_eq!(key, KEY_1); + assert_eq!(value, VAL_1); +} + +fn from_path_iter_unwrap_one_item(path: impl AsRef) -> (String, String) { + from_path_iter(path) + .expect("valid file") + .next() + .expect("one item") + .expect("valid item") +} diff --git a/dotenv/tests/integration/api/from_path_override.rs b/dotenv/tests/integration/api/from_path_override.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/dotenv/tests/integration/api/from_path_override.rs @@ -0,0 +1 @@ + diff --git a/dotenv/tests/integration/api/from_read.rs b/dotenv/tests/integration/api/from_read.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/dotenv/tests/integration/api/from_read.rs @@ -0,0 +1 @@ + diff --git a/dotenv/tests/integration/api/from_read_iter.rs b/dotenv/tests/integration/api/from_read_iter.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/dotenv/tests/integration/api/from_read_iter.rs @@ -0,0 +1 @@ + diff --git a/dotenv/tests/integration/api/from_read_override.rs b/dotenv/tests/integration/api/from_read_override.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/dotenv/tests/integration/api/from_read_override.rs @@ -0,0 +1 @@ + diff --git a/dotenv/tests/integration/api/var.rs b/dotenv/tests/integration/api/var.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/dotenv/tests/integration/api/var.rs @@ -0,0 +1 @@ + diff --git a/dotenv/tests/integration/api/vars.rs b/dotenv/tests/integration/api/vars.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/dotenv/tests/integration/api/vars.rs @@ -0,0 +1 @@ + diff --git a/dotenv/tests/integration/case/bom.rs b/dotenv/tests/integration/case/bom.rs new file mode 100644 index 00000000..ee233061 --- /dev/null +++ b/dotenv/tests/integration/case/bom.rs @@ -0,0 +1,54 @@ +use crate::util::*; +use dotenvy_test_util::*; + +#[test] +fn utf8_no_vars() { + let mut efb = EnvFileBuilder::new(); + efb.insert_utf8_bom(); + let testenv = TestEnv::init_with_envfile(efb); + test_keys_unset(&testenv); +} + +#[test] +fn utf8_one_var() { + let mut efb = EnvFileBuilder::new(); + efb.insert_utf8_bom(); + efb.add_key_value(KEY_1, VAL_1); + let testenv = TestEnv::init_with_envfile(efb); + test_key_1_only(&testenv); +} + +#[test] +fn utf8_two_vars() { + let mut efb = EnvFileBuilder::new(); + efb.insert_utf8_bom(); + let vars = [(KEY_1, VAL_1), (KEY_2, VAL_2)]; + efb.add_vars(&vars); + let testenv = TestEnv::init_with_envfile(efb); + test_env_vars(&testenv, &vars); +} + +#[test] +fn invalid_no_vars() { + let mut efb = EnvFileBuilder::new(); + efb.add_bytes(b"\xFA\xFA"); + let testenv = TestEnv::init_with_envfile(efb); + test_invalid_utf8(&testenv); +} + +#[test] +fn invalid_one_var() { + let mut efb = EnvFileBuilder::new(); + efb.add_bytes(b"\xFE\xFF"); + efb.add_key_value(KEY_1, VAL_1); + let testenv = TestEnv::init_with_envfile(efb); + test_invalid_utf8(&testenv); +} + +#[test] +fn utf16_no_vars() { + let mut efb = EnvFileBuilder::new(); + efb.add_bytes(b"\xFE\xFF"); + let testenv = TestEnv::init_with_envfile(efb); + test_invalid_utf8(&testenv); +} diff --git a/dotenv/tests/integration/case/comment.rs b/dotenv/tests/integration/case/comment.rs new file mode 100644 index 00000000..a04714b4 --- /dev/null +++ b/dotenv/tests/integration/case/comment.rs @@ -0,0 +1,56 @@ +use crate::util::*; +use dotenvy_test_util::*; + +#[test] +fn one() { + let testenv = TestEnv::init_with_envfile(format!("# {KEYVAL_1}")); + test_keys_unset(&testenv); +} + +#[test] +fn one_whitespace() { + let testenv = TestEnv::init_with_envfile(format!(" # {KEYVAL_1}")); + test_keys_unset(&testenv); +} + +#[test] +fn one_and_one_assign() { + let testenv = TestEnv::init_with_envfile(format!("# {KEYVAL_1}\n{KEYVAL_2}")); + test_key_2_only(&testenv); +} + +#[test] +fn one_and_one_assign_whitespace() { + let testenv = TestEnv::init_with_envfile(format!(" # {KEYVAL_1}\n{KEYVAL_2}")); + test_key_2_only(&testenv); +} + +#[test] +fn assign_same_line() { + let testenv = TestEnv::init_with_envfile(format!("{KEYVAL_1} # {KEYVAL_2}")); + test_key_1_only(&testenv); +} + +#[test] +fn hash_in_value() { + let testenv = TestEnv::init_with_envfile(format!("{KEYVAL_1}#{KEYVAL_2}")); + test_key_1_with_hash_val(&testenv); +} + +#[test] +fn hash_in_value_single_quoted() { + let testenv = TestEnv::init_with_envfile(format!("{KEY_1}='{VAL_1}'#{KEYVAL_2}")); + test_key_1_with_hash_val(&testenv); +} + +#[test] +fn hash_in_value_double_quoted() { + let testenv = TestEnv::init_with_envfile(format!(r##"{KEY_1}="{VAL_1}"#{KEYVAL_2}"##)); + test_key_1_with_hash_val(&testenv); +} + +#[test] +fn hash_in_key() { + let testenv = TestEnv::init_with_envfile("FOO#1=bar"); + test_err_line_parse(&testenv, "FOO#1=bar", 3); +} diff --git a/dotenv/tests/integration/case/directory.rs b/dotenv/tests/integration/case/directory.rs new file mode 100644 index 00000000..76ffdeb6 --- /dev/null +++ b/dotenv/tests/integration/case/directory.rs @@ -0,0 +1,44 @@ +use crate::util::*; +use dotenvy_test_util::*; + +#[test] +fn child_dir() { + let mut testenv = TestEnv::init_with_envfile(KEYVAL_1); + testenv.add_child_dir("child"); + testenv.set_work_dir("child"); + test_key_1_only(&testenv); +} + +#[test] +fn child_dir_no_envfile() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("child"); + testenv.set_work_dir("child"); + test_err_not_found(&testenv); +} + +#[test] +fn parent_dir_not_found() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("child"); + testenv.add_envfile("child/.env", KEYVAL_1); + test_err_not_found(&testenv); +} + +#[test] +fn sibling_dir_not_found() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("brother"); + testenv.add_child_dir("sister"); + testenv.add_envfile("brother/.env", KEYVAL_1); + testenv.set_work_dir("sister"); + test_err_not_found(&testenv); +} + +#[test] +fn grandchild_dir() { + let mut testenv = TestEnv::init_with_envfile(KEYVAL_1); + testenv.add_child_dir("child/grandchild"); + testenv.set_work_dir("child/grandchild"); + test_key_1_only(&testenv); +} diff --git a/dotenv/tests/integration/case/envfile.rs b/dotenv/tests/integration/case/envfile.rs new file mode 100644 index 00000000..bfb18ac9 --- /dev/null +++ b/dotenv/tests/integration/case/envfile.rs @@ -0,0 +1,73 @@ +use crate::util::*; +use dotenvy_test_util::*; + +const ONE_WORD: &str = "oneword"; + +#[test] +#[should_panic] +fn none() { + let testenv = TestEnv::init(); + test_in_env(&testenv, api_fn); +} + +#[test] +fn none_err() { + let testenv = TestEnv::init(); + test_err_not_found(&testenv); +} + +#[test] +fn empty() { + let testenv = create_empty_envfile_testenv(); + test_in_env(&testenv, || { + api_fn(); + assert_default_existing_var(); + }); +} + +#[test] +fn empty_path() { + let testenv = create_empty_envfile_testenv(); + test_default_envfile_path(&testenv); +} + +#[test] +#[should_panic] +fn one_word() { + let testenv = create_one_word_envfile_testenv(); + test_in_env(&testenv, api_fn); +} + +#[test] +fn one_word_err() { + let testenv = create_one_word_envfile_testenv(); + test_err_line_parse(&testenv, ONE_WORD, ONE_WORD.len()); +} + +#[test] +fn one_line() { + let testenv = create_one_line_envfile_testenv(); + test_key_1_only(&testenv); +} + +#[test] +fn one_line_path() { + let testenv = create_one_line_envfile_testenv(); + test_default_envfile_path(&testenv); +} + +fn create_empty_envfile_testenv() -> TestEnv { + let mut testenv = create_testenv_with_default_var(); + testenv.add_envfile(".env", ""); + testenv +} + +fn create_one_word_envfile_testenv() -> TestEnv { + let mut testenv = create_testenv_with_default_var(); + testenv.add_envfile(".env", ONE_WORD); + testenv +} + +fn create_one_line_envfile_testenv() -> TestEnv { + TestEnv::init_with_envfile(KEYVAL_1) +} diff --git a/dotenv/tests/integration/case/export.rs b/dotenv/tests/integration/case/export.rs new file mode 100644 index 00000000..cb3df5e8 --- /dev/null +++ b/dotenv/tests/integration/case/export.rs @@ -0,0 +1,20 @@ +use crate::util::*; +use dotenvy_test_util::*; + +#[test] +fn ignore_export() { + let testenv = TestEnv::init_with_envfile(format!("export {KEYVAL_1}")); + test_key_1_only(&testenv); +} + +#[test] +fn ignore_export_whitespace() { + let testenv = TestEnv::init_with_envfile(format!(" export {KEYVAL_1}")); + test_key_1_only(&testenv); +} + +#[test] +fn ignore_export_and_comment() { + let testenv = TestEnv::init_with_envfile(format!("export {KEYVAL_1} # {KEYVAL_2}")); + test_key_1_only(&testenv); +} diff --git a/dotenv/tests/integration/case/multiline.rs b/dotenv/tests/integration/case/multiline.rs new file mode 100644 index 00000000..4c780e42 --- /dev/null +++ b/dotenv/tests/integration/case/multiline.rs @@ -0,0 +1,62 @@ +use crate::util::*; +use dotenvy_test_util::*; + +#[test] +fn no_quote_two_lines() { + let testenv = TestEnv::init_with_envfile("FOOBAR=foo\nbar"); + test_err_line_parse(&testenv, "bar", 3); +} + +#[test] +fn double_quote_two_lines_no_close() { + let line = "FOOBAR=\"foo\nbar"; + let testenv = TestEnv::init_with_envfile(line); + test_err_line_parse(&testenv, line, 15); +} + +#[test] +fn single_quote_two_lines_no_close() { + let line = "FOOBAR='foo\nbar"; + let testenv = TestEnv::init_with_envfile(line); + test_err_line_parse(&testenv, line, 15); +} + +#[test] +fn double_quote_two_lines() { + let envfile = r#"FOOBAR="foo +bar""#; + let testenv = TestEnv::init_with_envfile(envfile); + test_single_key_val(&testenv, "FOOBAR", "foo\nbar"); +} + +#[test] +fn single_quote_two_lines() { + let testenv = TestEnv::init_with_envfile("FOOBAR='foo\nbar'"); + test_single_key_val(&testenv, "FOOBAR", "foo\nbar"); +} + +#[test] +fn double_quote_three_lines() { + let envfile = r#"FOOBAR="foo +bar +baz""#; + let testenv = TestEnv::init_with_envfile(envfile); + test_single_key_val(&testenv, "FOOBAR", "foo\nbar\nbaz"); +} + +const COMPLEX_VALUE: &str = "-BEGIN PRIVATE KEY-\n-END PRIVATE KEY-\n\"QUOTED\""; +const COMPLEX_VALUE_ESCAPED: &str = "-BEGIN PRIVATE KEY-\n-END PRIVATE KEY-\\n\\\"QUOTED\\\""; + +#[test] +fn complex_escaped_in_double_quotes() { + let envfile = format!("WEAK=\"{COMPLEX_VALUE_ESCAPED}\""); + let testenv = TestEnv::init_with_envfile(envfile); + test_single_key_val(&testenv, "WEAK", COMPLEX_VALUE); +} + +#[test] +fn complex_escaped_in_single_quotes() { + let envfile = format!("STRONG='{COMPLEX_VALUE_ESCAPED}'"); + let testenv = TestEnv::init_with_envfile(envfile); + test_single_key_val(&testenv, "STRONG", COMPLEX_VALUE_ESCAPED); +} diff --git a/dotenv/tests/integration/case/multiline_comment.rs b/dotenv/tests/integration/case/multiline_comment.rs new file mode 100644 index 00000000..53bbaea8 --- /dev/null +++ b/dotenv/tests/integration/case/multiline_comment.rs @@ -0,0 +1,64 @@ +use crate::util::*; +use dotenvy_test_util::*; + +#[test] +fn alone_single_quote() { + let testenv = + TestEnv::init_with_envfile(format!(r#"# {KEYVAL_1} Comment with single ' quote"#)); + test_keys_unset(&testenv) +} + +#[test] +fn alone_single_quote_with_space() { + let testenv = + TestEnv::init_with_envfile(format!(r#" # {KEYVAL_1} Comment with single ' quote"#)); + test_keys_unset(&testenv) +} + +#[test] +fn alone_double_quote() { + let testenv = + TestEnv::init_with_envfile(format!(r#"# {KEYVAL_1} Comment with double " quote"#)); + test_keys_unset(&testenv) +} + +#[test] +fn alone_double_quote_with_space() { + let testenv = + TestEnv::init_with_envfile(format!(r#" # {KEYVAL_1} Comment with double " quote"#)); + test_keys_unset(&testenv) +} + +#[test] +fn single_quote() { + let mut efb = EnvFileBuilder::new(); + efb.add_strln(r#"# Comment with single ' quote"#); + test_open_quote_comment(efb); +} + +#[test] +fn single_quote_with_space() { + let mut efb = EnvFileBuilder::new(); + efb.add_strln(r#" # Comment with single ' quote"#); + test_open_quote_comment(efb); +} + +#[test] +fn double_quote() { + let mut efb = EnvFileBuilder::new(); + efb.add_strln(r#"# Comment with double " quote"#); + test_open_quote_comment(efb); +} + +#[test] +fn double_quote_with_space() { + let mut efb = EnvFileBuilder::new(); + efb.add_strln(r#" # Comment with double " quote"#); + test_open_quote_comment(efb); +} + +fn test_open_quote_comment(mut efb: EnvFileBuilder) { + efb.add_strln(KEYVAL_1); + let testenv = TestEnv::init_with_envfile(efb); + test_key_1_only(&testenv); +} diff --git a/dotenv/tests/integration/case/quote.rs b/dotenv/tests/integration/case/quote.rs new file mode 100644 index 00000000..4c04251d --- /dev/null +++ b/dotenv/tests/integration/case/quote.rs @@ -0,0 +1,89 @@ +use crate::util::*; +use dotenvy_test_util::*; + +#[test] +fn double_value() { + let testenv = TestEnv::init_with_envfile(format!(r#"{KEY_1}="{VAL_1}""#)); + test_key_1_only(&testenv); +} + +#[test] +fn single_value() { + let testenv = TestEnv::init_with_envfile(format!(r"{KEY_1}='{VAL_1}'")); + test_key_1_only(&testenv); +} + +#[test] +fn double_and_single_value() { + let testenv = TestEnv::init_with_envfile(r#"fooo="'bar'""#); + test_single_key_val(&testenv, "fooo", "'bar'"); +} + +#[test] +fn single_and_double_value() { + let testenv = TestEnv::init_with_envfile(r#"fooo='"bar"'"#); + test_single_key_val(&testenv, "fooo", "\"bar\""); +} + +#[test] +fn double_key() { + let line = r#""FOOO"=bar"#; + let testenv = TestEnv::init_with_envfile(line); + test_err_line_parse(&testenv, line, 0); +} + +#[test] +fn single_key() { + let line = "'FOOO'=bar"; + let testenv = TestEnv::init_with_envfile(line); + test_err_line_parse(&testenv, line, 0); +} + +#[test] +fn double_in_double_value() { + let testenv = TestEnv::init_with_envfile(r#"FOOO="outer "inner"""#); + test_single_key_val(&testenv, "FOOO", "outer inner"); +} + +#[test] +fn double_in_single_value() { + let testenv = TestEnv::init_with_envfile(r#"FOOO='outer "inner"'"#); + test_single_key_val(&testenv, "FOOO", "outer \"inner\""); +} + +#[test] +fn single_in_double_value() { + let testenv = TestEnv::init_with_envfile(r#"FOOO="outer 'inner'""#); + test_single_key_val(&testenv, "FOOO", "outer 'inner'"); +} + +#[test] +fn single_in_single_value() { + let testenv = TestEnv::init_with_envfile("FOOO='outer 'inner''"); + test_single_key_val(&testenv, "FOOO", "outer inner"); +} + +#[test] +fn escaped_double_in_double_value() { + let testenv = TestEnv::init_with_envfile(r#"FOOO="outer \"inner\"""#); + test_single_key_val(&testenv, "FOOO", "outer \"inner\""); +} + +#[test] +fn escaped_double_in_single_value() { + let testenv = TestEnv::init_with_envfile(r#"FOOO='outer \"inner\"'"#); + test_single_key_val(&testenv, "FOOO", r#"outer \"inner\""#); +} + +#[test] +fn escaped_single_in_double_value() { + let testenv = TestEnv::init_with_envfile(r#"FOOO="outer \'inner\'""#); + test_single_key_val(&testenv, "FOOO", "outer 'inner'"); +} + +#[test] +fn escaped_single_in_single_value() { + let line = br"FOOO='outer \'inner\''"; + let testenv = TestEnv::init_with_envfile(*line); + test_err_line_parse(&testenv, "'outer \\'inner\\''", 16); +} diff --git a/dotenv/tests/integration/case/var_substitution.rs b/dotenv/tests/integration/case/var_substitution.rs new file mode 100644 index 00000000..2ebdb6b2 --- /dev/null +++ b/dotenv/tests/integration/case/var_substitution.rs @@ -0,0 +1,249 @@ +use crate::util::*; +use dotenvy_test_util::*; + +mod no_quotes { + use super::*; + + #[test] + fn from_env() { + let envfile = format!("{KEY_1}=${KEY_2}"); + test_key_1_set_val_2(&envfile); + } + + #[test] + fn plus_extra() { + let envfile = format!("{KEY_1}=${KEY_2}+extra"); + test_key_1_set_val_2_plus_extra(&envfile); + } + + #[test] + fn plus_space() { + let envfile = format!("{KEY_1}=${KEY_2} + extra"); + let testenv = create_testenv_with_key_2(&envfile); + let expected = format!("${KEY_2} + extra"); + test_err_line_parse(&testenv, &expected, 8); + } + + #[test] + fn braced() { + let envfile = format!("{KEY_1}=${{{KEY_2}}}"); + test_key_1_set_val_2(&envfile); + } + + #[test] + fn braced_plus() { + let envfile = format!("{KEY_1}=${{{KEY_2}}}1"); + let expected = format!("{VAL_2}1"); + test_key_1_set_as_with_key_2(&envfile, &expected); + } + + #[test] + fn escaped() { + let envfile = format!("{KEY_1}=\\${KEY_2}"); + let expected = format!("${KEY_2}"); + test_key_1_set_as_with_key_2(&envfile, &expected); + } + + #[test] + fn key_not_set() { + let envfile = format!("{KEY_1}=${KEY_2}"); + let testenv = TestEnv::init_with_envfile(envfile); + test_single_key_val(&testenv, KEY_1, ""); + } + + #[test] + fn prev_entry() { + let envfile = format!("{KEYVAL_1}\n{KEY_2}=${KEY_1}"); + let testenv = TestEnv::init_with_envfile(envfile); + test_env_vars(&testenv, &[(KEY_1, VAL_1), (KEY_2, VAL_1)]); + } + + #[test] + fn prev_entry_plus_extra() { + let envfile = format!("{KEYVAL_1}\n{KEY_2}=${KEY_1}+extra"); + let testenv = TestEnv::init_with_envfile(envfile); + let expected_val_2 = format!("{VAL_1}+extra"); + test_env_vars(&testenv, &[(KEY_1, VAL_1), (KEY_2, &expected_val_2)]); + } +} + +mod double_quote { + use super::*; + + #[test] + fn from_env() { + let envfile = format!(r#"{KEY_1}="${KEY_2}"#); + let mut testenv = TestEnv::init_with_envfile(envfile.as_str()); + testenv.add_env_var(KEY_2, VAL_2); + test_err_line_parse(&testenv, &envfile, 11); + } + + #[test] + fn plus_extra() { + let envfile = format!(r#"{KEY_1}="${KEY_2}+extra""#); + test_key_1_set_val_2_plus_extra(&envfile); + } + + #[test] + fn plus_space() { + let envfile = format!(r#"{KEY_1}="${KEY_2} + extra""#); + test_key_1_set_val_2_plus_extra_with_space(&envfile); + } + + #[test] + fn braced() { + let envfile = format!("{KEY_1}=\"${{{KEY_2}}}\""); + test_key_1_set_val_2(&envfile); + } + + #[test] + fn braced_plus() { + let envfile = format!(r#"{KEY_1}="${{{KEY_2}}}1""#); + let expected = format!("{VAL_2}1"); + test_key_1_set_as_with_key_2(&envfile, &expected); + } + + #[test] + fn escaped() { + let envfile = format!(r#"{KEY_1}="\\${KEY_2}""#); + let testenv = create_testenv_with_key_2(&envfile); + let expected = format!(r#""\\${KEY_2}""#); + test_err_line_parse(&testenv, &expected, 8); + } + + #[test] + fn escaped_plus() { + let envfile = format!(r#"{KEY_1}="\\${KEY_2}+1""#); + let expected = format!("\\{VAL_2}+1"); + test_key_1_set_as_with_key_2(&envfile, &expected); + } + + #[test] + fn key_not_set() { + let envfile = format!(r#"{KEY_1}="${KEY_2}""#); + let testenv = TestEnv::init_with_envfile(envfile); + let expected = format!(r#""${KEY_2}""#); + test_err_line_parse(&testenv, &expected, 6); + } + + #[test] + fn key_not_set_plus_extra() { + let envfile = format!(r#"{KEY_1}="${KEY_2}+extra""#); + let testenv = TestEnv::init_with_envfile(envfile); + test_single_key_val(&testenv, KEY_1, "+extra"); + } + + #[test] + fn prev_entry() { + let envfile = format!("{KEYVAL_1}\n{KEY_2}=\"${KEY_1}\""); + let testenv = TestEnv::init_with_envfile(envfile); + let expected = format!(r#""${KEY_1}""#); + test_err_line_parse(&testenv, &expected, 6); + } + + #[test] + fn prev_entry_plus_extra() { + let envfile = format!("{KEYVAL_1}\n{KEY_2}=\"${KEY_1}+extra\""); + let testenv = TestEnv::init_with_envfile(envfile); + let expected_val_2 = format!("{VAL_1}+extra"); + test_env_vars(&testenv, &[(KEY_1, VAL_1), (KEY_2, &expected_val_2)]); + } +} + +mod single_quote { + use super::*; + + #[test] + fn from_env() { + let envfile = format!("{KEY_1}='${KEY_2}'"); + let expected = format!("${KEY_2}"); + test_key_1_set_as_with_key_2(&envfile, &expected); + } + + #[test] + fn plus_extra() { + let envfile = format!("{KEY_1}='${KEY_2}+extra'"); + let expected = format!("${KEY_2}+extra"); + test_key_1_set_as_with_key_2(&envfile, &expected); + } + + #[test] + fn plus_space() { + let envfile = format!("{KEY_1}='${KEY_2} + extra'"); + let expected = format!("${KEY_2} + extra"); + test_key_1_set_as_with_key_2(&envfile, &expected); + } + + #[test] + fn braced() { + let envfile = format!("{KEY_1}='${{{KEY_2}}}'"); + let expected = format!("${{{KEY_2}}}"); + test_key_1_set_as_with_key_2(&envfile, &expected); + } + + #[test] + fn braced_plus() { + let envfile = format!("{KEY_1}='${{{KEY_2}}}1'"); + let expected = format!("${{{KEY_2}}}1"); + test_key_1_set_as_with_key_2(&envfile, &expected); + } + + #[test] + fn escaped() { + let envfile = format!("{KEY_1}='\\${KEY_2}'"); + let expected = format!("\\${KEY_2}"); + test_key_1_set_as_with_key_2(&envfile, &expected); + } + + #[test] + fn key_not_set() { + let envfile = format!("{KEY_1}='${KEY_2}'"); + let testenv = TestEnv::init_with_envfile(envfile); + let expected = format!("${KEY_2}"); + test_single_key_val(&testenv, KEY_1, &expected); + } + + #[test] + fn prev_entry() { + let envfile = format!("{KEYVAL_1}\n{KEY_2}='${KEY_1}'"); + let testenv = TestEnv::init_with_envfile(envfile); + let expected = format!("${KEY_1}"); + test_env_vars(&testenv, &[(KEY_1, VAL_1), (KEY_2, &expected)]); + } + + #[test] + fn prev_entry_plus_extra() { + let envfile = format!("{KEYVAL_1}\n{KEY_2}='${KEY_1}+extra'"); + let testenv = TestEnv::init_with_envfile(envfile); + let expected = format!("${KEY_1}+extra"); + test_env_vars(&testenv, &[(KEY_1, VAL_1), (KEY_2, &expected)]); + } +} + +fn test_key_1_set_as_with_key_2(envfile: &str, expected: &str) { + let testenv = create_testenv_with_key_2(envfile); + test_single_key_val(&testenv, KEY_1, expected); +} + +fn test_key_1_set_val_2(envfile: &str) { + let testenv = create_testenv_with_key_2(envfile); + test_single_key_val(&testenv, KEY_1, VAL_2); +} + +fn test_key_1_set_val_2_plus_extra(envfile: &str) { + let testenv = create_testenv_with_key_2(envfile); + let exepcted = format!("{VAL_2}+extra"); + test_single_key_val(&testenv, KEY_1, &exepcted); +} + +fn test_key_1_set_val_2_plus_extra_with_space(envfile: &str) { + let testenv = create_testenv_with_key_2(envfile); + let exepcted = format!("{VAL_2} + extra"); + test_single_key_val(&testenv, KEY_1, &exepcted); +} + +fn create_testenv_with_key_2(envfile: &str) -> TestEnv { + let mut testenv = TestEnv::init_with_envfile(envfile); + testenv.add_env_var(KEY_2, VAL_2); + testenv +} diff --git a/dotenv/tests/integration/case/whitespace.rs b/dotenv/tests/integration/case/whitespace.rs new file mode 100644 index 00000000..4b99fdb8 --- /dev/null +++ b/dotenv/tests/integration/case/whitespace.rs @@ -0,0 +1,64 @@ +use crate::util::*; +use dotenvy_test_util::*; + +#[test] +fn ignore_left() { + let envfiles = [ + " FOOO=bar", + "\nFOOO=bar", + "\r\n FOOO=bar", + "\n\nFOOO=bar", + "\r\n\r\nFOOO=bar", + "\tFOOO=bar", + " \r\n\t FOOO=bar", + ]; + test_whitespace_envfiles(&envfiles, "FOOO", "bar"); +} + +#[test] +fn ignore_right() { + let envfiles = [ + "FOOO=bar ", + "FOOO=bar\n", + "FOOO=bar\n\n", + "FOOO=bar\r\n", + "FOOO=bar\r\n\r\n", + "FOOO=bar \t", + "FOOO=bar \t \n", + "FOOO=bar \t \r\n", + // TODO: This should be allowed. + // "FOOO=bar\t", + ]; + test_whitespace_envfiles(&envfiles, "FOOO", "bar"); +} + +#[test] +fn around_assignment() { + let testenv = TestEnv::init_with_envfile(format!("{KEY_1} = {VAL_1}")); + test_key_1_only(&testenv); +} + +#[test] +fn escaped_in_value() { + let testenv = TestEnv::init_with_envfile(r"FOOO=foo\ bar\ baz"); + test_single_key_val(&testenv, "FOOO", "foo bar baz"); +} + +#[test] +fn double_quoted_value() { + let testenv = TestEnv::init_with_envfile(r#"FOOO="foo bar baz""#); + test_single_key_val(&testenv, "FOOO", "foo bar baz"); +} + +#[test] +fn single_quoted_value() { + let testenv = TestEnv::init_with_envfile("FOOO='foo bar baz'"); + test_single_key_val(&testenv, "FOOO", "foo bar baz"); +} + +fn test_whitespace_envfiles(envfiles: &[&str], key: &str, expected: &str) { + for &envfile in envfiles { + let testenv = TestEnv::init_with_envfile(envfile); + test_single_key_val(&testenv, key, expected); + } +} diff --git a/dotenv/tests/integration/main.rs b/dotenv/tests/integration/main.rs index 83c8c0aa..deb5e351 100644 --- a/dotenv/tests/integration/main.rs +++ b/dotenv/tests/integration/main.rs @@ -1 +1,34 @@ -mod util; +/// `dotenvy` integration tests +mod api { + mod dotenv; + mod dotenv_iter; + mod dotenv_override; + mod from_filename; + mod from_filename_iter; + mod from_filename_override; + mod from_path; + mod from_path_iter; + mod from_path_override; + mod from_read; + mod from_read_iter; + mod from_read_override; + mod var; + mod vars; +} + +/// different environment-setup test-cases +mod case { + mod bom; + mod comment; + mod directory; + mod envfile; + mod export; + mod multiline; + mod multiline_comment; + mod quote; + mod var_substitution; + mod whitespace; +} + +/// constructors, helpers, and assertions +pub(crate) mod util; diff --git a/dotenv/tests/integration/util/assert.rs b/dotenv/tests/integration/util/assert.rs new file mode 100644 index 00000000..5d46196b --- /dev/null +++ b/dotenv/tests/integration/util/assert.rs @@ -0,0 +1,37 @@ +use std::{io, path::Path}; + +use dotenvy::Error; +use dotenvy_test_util::*; + +pub fn assert_err_line_parse(line: &str, index: usize, actual: Error) { + match actual { + Error::LineParse(s, i) => { + assert_eq!(line, s, "expected line parse error for line `{line}`"); + assert_eq!(index, i, "expected line parse error at index {index}"); + } + _ => panic!("expected line parse error"), + } +} + +pub fn assert_err_not_found(actual: Error) { + match actual { + Error::Io(err) => assert_eq!(err.kind(), io::ErrorKind::NotFound), + _ => panic!("expected `NotFound` error"), + } +} + +pub fn assert_err_invalid_utf8(actual: Error) { + match actual { + Error::Io(err) => assert_eq!(err.kind(), io::ErrorKind::InvalidData), + _ => panic!("expected `InvalidData` error"), + } +} + +pub fn assert_default_envfile_path(testenv: &TestEnv, path: &Path) { + let expected = testenv + .temp_path() + .join(".env") + .canonicalize() + .expect("failed to canonicalize"); + assert_eq!(expected, path); +} diff --git a/dotenv/tests/integration/util/create.rs b/dotenv/tests/integration/util/create.rs new file mode 100644 index 00000000..e69de29b diff --git a/dotenv/tests/integration/util/mod.rs b/dotenv/tests/integration/util/mod.rs index 12430b63..77d3b460 100644 --- a/dotenv/tests/integration/util/mod.rs +++ b/dotenv/tests/integration/util/mod.rs @@ -1,67 +1,69 @@ -#![allow(dead_code)] +use std::{ + collections::HashMap, + fs::File, + path::{Path, PathBuf}, +}; -mod testenv; +use dotenvy::{Error, Iter, Result}; +use dotenvy_test_util::*; -use std::env::{self, VarError}; +/// common assertions +mod assert; +/// particular tests in a testenv +mod test; -/// Default key used in envfile -pub const TEST_KEY: &str = "TESTKEY"; -/// Default value used in envfile -pub const TEST_VALUE: &str = "test_val"; +pub use assert::*; +pub use test::*; -/// Default existing key set before test is run -pub const TEST_EXISTING_KEY: &str = "TEST_EXISTING_KEY"; -/// Default existing value set before test is run -pub const TEST_EXISTING_VALUE: &str = "from_env"; -/// Default overriding value in envfile -pub const TEST_OVERRIDING_VALUE: &str = "from_file"; +pub const KEY_1: &str = "FOOO"; +pub const VAL_1: &str = "bar"; +pub const KEYVAL_1: &str = "FOOO=bar"; -#[inline(always)] -pub fn create_default_envfile() -> String { - format!( - "{}={}\n{}={}", - TEST_KEY, TEST_VALUE, TEST_EXISTING_KEY, TEST_OVERRIDING_VALUE - ) +pub const KEY_2: &str = "BARR"; +pub const VAL_2: &str = "foo"; +pub const KEYVAL_2: &str = "BARR=foo"; + +/// Call and unwrap the default api function +pub fn api_fn() { + dotenvy::dotenv().unwrap(); +} + +/// Call, unwrap and return the `PathBuf` of the default api function +pub fn api_fn_path() -> PathBuf { + dotenvy::dotenv().unwrap() } -/// missing equals -#[inline(always)] -pub fn create_invalid_envfile() -> String { - format!( - "{}{}\n{}{}", - TEST_KEY, TEST_VALUE, TEST_EXISTING_KEY, TEST_OVERRIDING_VALUE - ) +/// Call, unwrap and return the `Error` of the default api function +pub fn api_fn_err() -> Error { + dotenvy::dotenv().unwrap_err() } -/// Assert that an environment variable is set and has the expected value. -pub fn assert_env_var(key: &str, expected: &str) { - match env::var(key) { - Ok(actual) => assert_eq!( - expected, actual, - "\n\nFor Environment Variable `{}`:\n EXPECTED: `{}`\n ACTUAL: `{}`\n", - key, expected, actual - ), - Err(VarError::NotPresent) => panic!("env var `{}` not found", key), - Err(VarError::NotUnicode(val)) => panic!( - "env var `{}` currently has invalid unicode: `{}`", - key, - val.to_string_lossy() - ), - } +pub fn check_iter_default_envfile_into_hash_map(iter_fn: F) +where + F: FnOnce() -> Result>, +{ + let vars = [("FOOO", "bar"), ("BAZ", "qux")]; + let envfile = create_custom_envfile(&vars); + let testenv = TestEnv::init_with_envfile(envfile); + + test_in_env(&testenv, || { + let map: HashMap = iter_fn() + .expect("valid file") + .map(|item| item.expect("valid item")) + .collect(); + + for (key, expected) in vars { + let actual = map.get(key).expect("valid key"); + assert_eq!(expected, actual); + } + }); } -/// Assert that an environment variable is not currently set. -pub fn assert_env_var_unset(key: &str) { - match env::var(key) { - Ok(actual) => panic!( - "env var `{}` should not be set, currently it is: `{}`", - key, actual - ), - Err(VarError::NotUnicode(val)) => panic!( - "env var `{}` should not be set, currently has invalid unicode: `{}`", - key, - val.to_string_lossy() - ), - _ => (), - } +/// Relative to the temp dir +pub fn canonicalize_envfile_path(testenv: &TestEnv, envfile: impl AsRef) -> PathBuf { + testenv + .temp_path() + .join(envfile.as_ref()) + .canonicalize() + .expect("canonicalize envfile") } diff --git a/dotenv/tests/integration/util/test.rs b/dotenv/tests/integration/util/test.rs new file mode 100644 index 00000000..d496736e --- /dev/null +++ b/dotenv/tests/integration/util/test.rs @@ -0,0 +1,75 @@ +use super::*; + +pub fn test_default_envfile_path(testenv: &TestEnv) { + test_in_env(testenv, || { + let path = api_fn_path(); + assert_default_envfile_path(testenv, &path); + }) +} + +pub fn test_err_line_parse(testenv: &TestEnv, line: &str, index: usize) { + test_in_env(testenv, || { + let err = api_fn_err(); + assert_err_line_parse(line, index, err); + }) +} + +pub fn test_err_not_found(testenv: &TestEnv) { + test_in_env(testenv, || { + let err = api_fn_err(); + assert_err_not_found(err); + }) +} + +pub fn test_invalid_utf8(testenv: &TestEnv) { + test_in_env(testenv, || { + let err = api_fn_err(); + assert_err_invalid_utf8(err); + }) +} + +pub fn test_key_1_only(testenv: &TestEnv) { + test_in_env(testenv, || { + api_fn(); + assert_env_var(KEY_1, VAL_1); + assert_env_var_unset(KEY_2); + }); +} + +pub fn test_key_2_only(testenv: &TestEnv) { + test_in_env(testenv, || { + api_fn(); + assert_env_var_unset(KEY_1); + assert_env_var(KEY_2, VAL_2); + }); +} + +pub fn test_keys_unset(testenv: &TestEnv) { + test_in_env(testenv, || { + api_fn(); + assert_env_var_unset(KEY_1); + assert_env_var_unset(KEY_2); + }); +} + +pub fn test_key_1_with_hash_val(testenv: &TestEnv) { + test_in_env(testenv, || { + api_fn(); + assert_env_var(KEY_1, &format!("{VAL_1}#{KEYVAL_2}")); + assert_env_var_unset(KEY_2); + }); +} + +pub fn test_single_key_val(testenv: &TestEnv, key: &str, expected: &str) { + test_in_env(testenv, || { + api_fn(); + assert_env_var(key, expected); + }); +} + +pub fn test_env_vars(testenv: &TestEnv, vars: &[(&str, &str)]) { + test_in_env(testenv, || { + api_fn(); + assert_env_vars(vars) + }); +} diff --git a/dotenv/tests/integration/util/testenv.rs b/dotenv/tests/integration/util/testenv.rs deleted file mode 100644 index a3d251bc..00000000 --- a/dotenv/tests/integration/util/testenv.rs +++ /dev/null @@ -1,337 +0,0 @@ -use super::{create_default_envfile, TEST_EXISTING_KEY, TEST_EXISTING_VALUE}; -use once_cell::sync::OnceCell; -use std::{ - collections::HashMap, - env, fs, - io::{self, Write}, - path::{Path, PathBuf}, - sync::{Arc, Mutex, PoisonError}, -}; -use tempfile::{tempdir, TempDir}; - -/// Env var convenience type. -type EnvMap = HashMap; - -/// Initialized in [`get_env_locker`] -static ENV_LOCKER: OnceCell>> = OnceCell::new(); - -/// A test environment. -/// -/// Will create a new temporary directory. Use its builder methods to configure -/// the directory structure, preset variables, envfile name and contents, and -/// the working directory to run the test from. -/// -/// Creation methods: -/// - [`TestEnv::init`]: blank environment (no envfile) -/// - [`TestEnv::init_with_envfile`]: blank environment with an envfile -/// - [`TestEnv::default`]: default testing environment (1 existing var and 2 -/// set in a `.env` file) -#[derive(Debug)] -pub struct TestEnv { - temp_dir: TempDir, - work_dir: PathBuf, - env_vars: Vec, - envfile_contents: Option, - envfile_path: PathBuf, -} - -/// Simple key value struct for representing environment variables -#[derive(Debug, Clone)] -pub struct KeyVal { - key: String, - value: String, -} - -/// Run a test closure within a test environment. -/// -/// Resets the environment variables, loads the [`TestEnv`], then runs the test -/// closure. Ensures only one thread has access to the process environment. -pub fn test_in_env(test_env: TestEnv, test: F) -where - F: FnOnce(), -{ - let locker = get_env_locker(); - // ignore a poisoned mutex - // we expect some tests may panic to indicate a failure - let original_env = locker.lock().unwrap_or_else(PoisonError::into_inner); - // we reset the environment anyway upon acquiring the lock - reset_env(&original_env); - create_env(&test_env); - test(); - // drop the lock and the `TestEnv` - should delete the tempdir -} - -/// Run a test closure within the default test environment. -/// -/// Resets the environment variables, creates the default [`TestEnv`], then runs -/// the test closure. Ensures only one thread has access to the process -/// environment. -/// -/// The default testing environment sets an existing environment variable -/// `TEST_EXISTING_KEY`, which is set to `from_env`. It also creates a `.env` -/// file with the two lines: -/// -/// ```ini -/// TESTKEY=test_val -/// TEST_EXISTING_KEY=from_file -/// ``` -/// -/// Notice that file has the potential to override `TEST_EXISTING_KEY` depending -/// on the what's being tested. -pub fn test_in_default_env(test: F) -where - F: FnOnce(), -{ - let test_env = TestEnv::default(); - test_in_env(test_env, test); -} - -impl TestEnv { - /// Blank testing environment in a new temporary directory. - /// - /// No envfile_contents or pre-existing variables to set. The envfile_name - /// is set to `.env` but won't be written until its content is set. The - /// working directory is the created temporary directory. - pub fn init() -> Self { - let tempdir = tempdir().expect("create tempdir"); - let work_dir = tempdir.path().to_owned(); - let envfile_path = work_dir.join(".env"); - Self { - temp_dir: tempdir, - work_dir, - env_vars: Default::default(), - envfile_contents: None, - envfile_path, - } - } - - /// Testing environment with custom envfile_contents. - /// - /// No pre-existing env_vars set. The envfile_name is set to `.env`. The - /// working directory is the created temporary directory. - pub fn init_with_envfile(contents: impl ToString) -> Self { - let mut test_env = Self::init(); - test_env.set_envfile_contents(contents); - test_env - } - - /// Change the name of the default `.env` file. - /// - /// It will still be placed in the root temporary directory. If you need to - /// put the envfile in a different directory, use - /// [`set_envfile_path`](TestEnv::set_envfile_path) instead. - pub fn set_envfile_name(&mut self, name: impl AsRef) -> &mut Self { - self.envfile_path = self.temp_path().join(name); - self - } - - /// Change the absolute path to the envfile. - pub fn set_envfile_path(&mut self, path: PathBuf) -> &mut Self { - self.envfile_path = path; - self - } - - /// Specify the contents of the envfile. - /// - /// If this is the only change to the [`TestEnv`] being made, use - /// [`new_with_envfile`](TestEnv::new_with_envfile). - /// - /// Setting it to an empty string will cause an empty envfile to be created - pub fn set_envfile_contents(&mut self, contents: impl ToString) -> &mut Self { - self.envfile_contents = Some(contents.to_string()); - self - } - - /// Set the working directory the test will run from. - /// - /// The default is the created temporary directory. This method is useful if - /// you wish to run a test from a subdirectory or somewhere else. - pub fn set_work_dir(&mut self, path: PathBuf) -> &mut Self { - self.work_dir = path; - self - } - - /// Add an individual environment variable. - /// - /// This adds more pre-existing environment variables to the process before - /// any tests are run. - pub fn add_env_var(&mut self, key: impl ToString, value: impl ToString) -> &mut Self { - self.env_vars.push(KeyVal { - key: key.to_string(), - value: value.to_string(), - }); - self - } - - /// Set the pre-existing environment variables. - /// - /// These variables will get added to the process' environment before the - /// test is run. This overrides any previous env vars added to the - /// [`TestEnv`]. - /// - /// If you wish to just use a slice of tuples, use - /// [`set_env_vars_tuple`](TestEnv::set_env_vars_tuple) instead. - pub fn set_env_vars(&mut self, env_vars: Vec) -> &mut Self { - self.env_vars = env_vars; - self - } - - /// Set the pre-existing environment variables using [`str`] tuples. - /// - /// These variables will get added to the process' environment before the - /// test is run. This overrides any previous env vars added to the - /// [`TestEnv`]. - /// - /// If you wish to add an owned `Vec` instead of `str` tuples, use - /// [`set_env_vars`](TestEnv::set_env_vars) instead. - pub fn set_env_vars_tuple(&mut self, env_vars: &[(&str, &str)]) -> &mut Self { - self.env_vars = env_vars - .iter() - .map(|(key, value)| KeyVal { - key: key.to_string(), - value: value.to_string(), - }) - .collect(); - - self - } - - /// Create a child folder within the temporary directory. - /// - /// This will not change the working directory the test is run in, or where - /// the envfile is created. - /// - /// Will create parent directories if they are missing. - pub fn add_child_dir_all(&self, rel_path: impl AsRef) -> PathBuf { - let rel_path = rel_path.as_ref(); - let child_dir = self.temp_path().join(rel_path); - if let Err(err) = fs::create_dir_all(&child_dir) { - panic!( - "unable to create child directory: `{}` in `{}`: {}", - self.temp_path().display(), - rel_path.display(), - err - ); - } - child_dir - } - - /// Get a reference to the path of the temporary directory. - pub fn temp_path(&self) -> &Path { - self.temp_dir.path() - } - - /// Get a reference to the working directory the test will be run from. - pub fn work_dir(&self) -> &Path { - &self.work_dir - } - - /// Get a reference to environment variables that will be set **before** - /// the test. - pub fn env_vars(&self) -> &[KeyVal] { - &self.env_vars - } - - /// Get a reference to the string that will be placed in the envfile. - /// - /// If `None` is returned, an envfile will not be created - pub fn envfile_contents(&self) -> Option<&str> { - self.envfile_contents.as_deref() - } - - /// Get a reference to the path of the envfile. - pub fn envfile_path(&self) -> &Path { - &self.envfile_path - } -} - -impl Default for TestEnv { - fn default() -> Self { - let temp_dir = tempdir().expect("create tempdir"); - let work_dir = temp_dir.path().to_owned(); - let env_vars = vec![KeyVal { - key: TEST_EXISTING_KEY.into(), - value: TEST_EXISTING_VALUE.into(), - }]; - let envfile_contents = Some(create_default_envfile()); - let envfile_path = work_dir.join(".env"); - Self { - temp_dir, - work_dir, - env_vars, - envfile_contents, - envfile_path, - } - } -} - -impl From<(&str, &str)> for KeyVal { - fn from(kv: (&str, &str)) -> Self { - let (key, value) = kv; - Self { - key: key.to_string(), - value: value.to_string(), - } - } -} - -impl From<(String, String)> for KeyVal { - fn from(kv: (String, String)) -> Self { - let (key, value) = kv; - Self { key, value } - } -} - -/// Get a guarded copy of the original process' env vars. -fn get_env_locker() -> Arc> { - Arc::clone(ENV_LOCKER.get_or_init(|| { - let map: EnvMap = env::vars().collect(); - Arc::new(Mutex::new(map)) - })) -} - -/// Reset the process' env vars back to what was in `original_env`. -fn reset_env(original_env: &EnvMap) { - // remove keys if they weren't in the original environment - env::vars() - .filter(|(key, _)| !original_env.contains_key(key)) - .for_each(|(key, _)| env::remove_var(key)); - // ensure original keys have their original values - original_env - .iter() - .for_each(|(key, value)| env::set_var(key, value)); -} - -/// Create an environment to run tests in. -/// -/// Writes the envfile, sets the working directory, and sets environment vars. -fn create_env(test_env: &TestEnv) { - // only create the envfile if its contents has been set - if let Some(contents) = test_env.envfile_contents() { - create_envfile(&test_env.envfile_path, contents); - } - - env::set_current_dir(&test_env.work_dir).expect("setting working directory"); - - for KeyVal { key, value } in &test_env.env_vars { - env::set_var(key, value) - } -} - -/// Create an envfile for use in tests. -fn create_envfile(path: &Path, contents: &str) { - if path.exists() { - panic!("envfile `{}` already exists", path.display()) - } - // inner function to group together io::Results - fn create_env_file_inner(path: &Path, contents: &str) -> io::Result<()> { - let mut file = fs::File::create(path)?; - file.write_all(contents.as_bytes())?; - file.sync_all() - } - // call inner function - if let Err(err) = create_env_file_inner(path, contents) { - // handle any io::Result::Err - panic!("error creating envfile `{}`: {}", path.display(), err); - } -} diff --git a/test_util/Cargo.toml b/test_util/Cargo.toml new file mode 100644 index 00000000..8368d21e --- /dev/null +++ b/test_util/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "dotenvy_test_util" +version = "0.1.0" +authors = [ + "Christopher Morton ", +] +description = "Test utilities for dotenvy" +homepage = "https://github.com/allan2/dotenvy" +readme = "README.md" +keywords = ["dotenv", "testing", "utility", "util", "harness"] +categories = ["development-tools", "development-tools::testing"] +license = "MIT" +repository = "https://github.com/allan2/dotenvy" +edition = "2021" +rust-version = "1.68.0" + +[dependencies] +dotenvy = { path = "../dotenv", version = "0.15.7" } +tempfile = "3.3.0" +once_cell = "1.16.0" diff --git a/test_util/LICENSE b/test_util/LICENSE new file mode 100644 index 00000000..1fc6dfdc --- /dev/null +++ b/test_util/LICENSE @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright (c) 2024 Christopher Morton and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/test_util/README.md b/test_util/README.md new file mode 100644 index 00000000..f3dad08e --- /dev/null +++ b/test_util/README.md @@ -0,0 +1,135 @@ +# dotenvy test util + +This is an internal package used for testing dotenvy. + +## Why + +Eases the annoyance of setting up custom `.env` files, managing existing +environment variables, and running multiple tests at once. + +## How + +By setting up a `TestEnv`, and running a closure via `test_in_env`. + +**Before** executing the closure, the `TestEnv` will: + +- Create a temporary directory +- Lock the environment from other tests +- Store the existing environment variables +- Add any custom env_vars to the environment +- Create any custom envfiles in the temporary directory + +**In the closure** you can use the assertion functions to test the environment. + +**After** executing the closure, the `TestEnv` will: + +- Remove the temporary directory +- Restore the environment variables to the original state +- Unlock the environment + +See the API docs for more details. For now, they must be built locally with +`cargo doc`. + +### Commented example + +```rust +use dotenvy_test_util::*; +use dotenvy::dotenv_override; + +const EXISTING_KEY: &str = "TEST_KEY"; +const EXISTING_VAL: &str = "test_val"; +const OVERRIDING_VAL: &str = "overriding_val"; + +#[test] +fn dotenv_override_existing_key() { + // setup testing environment + let mut testenv = TestEnv::init(); + + // with an existing environment variable + testenv.add_env_var(EXISTING_KEY, EXISTING_VAL); + + // with an envfile that overrides it + testenv.add_envfile( + ".env", + create_custom_envfile(&[(EXISTING_KEY, OVERRIDING_VAL)]), + ); + + // execute a closure in the testing environment + test_in_env(&testenv, || { + dotenv_override().expect(".env should be loaded"); + assert_env_var(EXISTING_KEY, OVERRIDING_VAL); + }); + // any changes to environment variables will be reset for other tests +} +``` + +### Default TestEnv + +Use the default `TestEnv` for simple tests. + +```rust +use dotenvy_test_util::*; +use dotenvy::dotenv; + +#[test] +fn dotenv_works() { + test_in_default_env(|| { + dotenv().expect(".env should be loaded"); + assert_env_var(DEFAULT_KEY, DEFAULT_VAL); + }) +} +``` + +The default `TestEnv` has 1 existing environment variable: + +- `DEFAULT_EXISTING_KEY` set to `DEFAULT_EXISTING_VAL` + +It has an envfile `.env` that sets: + +- `DEFAULT_TEST_KEY` to `DEFAULT_TEST_VAL` +- `DEFAULT_EXISTING_KEY` to `DEFAULT_OVERRIDING_VAL` + +### Customised Envfile + +Use the `EnvFileBuilder` to manipulate the content of an envfile. Useful +for byte-order-mark(BOM) testing, and other edge cases. + +```rust +use dotenvy_test_util::*; +use dotenvy::dotenv; + +#[test] +fn comments_ignored_in_utf8bom_envfile() { + let mut efb = EnvFileBuilder::new(); + efb.insert_utf8_bom(); + efb.add_strln("# TEST_KEY=TEST_VAL"); + + let testenv = TestEnv::init_with_envfile(efb); + + test_in_env(&testenv, || { + dotenv().expect(".env should be loaded"); + assert_env_var_unset("TEST_KEY"); + }); +} +``` + +Or use anything that can be converted to a `Vec` if your envfile is +simple. + +```rust +use dotenvy_test_util::*; +use dotenvy::dotenv; + +#[test] +fn comments_ignored() { + let envfile = "# TEST_KEY=TEST_VAL\n"; + + let testenv = TestEnv::init_with_envfile(envfile); + + test_in_env(&testenv, || { + dotenv().expect(".env should be loaded"); + assert_env_var_unset("TEST_KEY"); + }); +} +``` + diff --git a/test_util/src/assertions.rs b/test_util/src/assertions.rs new file mode 100644 index 00000000..8a169a62 --- /dev/null +++ b/test_util/src/assertions.rs @@ -0,0 +1,75 @@ +use super::*; +use std::env::{self, VarError}; + +/// Assert multiple environment variables are set and have the expected +/// values. +/// +/// ## Arguments +/// +/// * `vars` - A slice of `(key, expected_value)` tuples +/// +/// ## Example +/// +/// ```no_run +/// # use dotenvy_test_util::assert_env_vars; +/// assert_env_vars(&[ +/// ("DEFAULT_TEST_KEY", "default_test_val"), +/// ("DEFAULT_EXISTING_KEY", "loaded_from_env"), +/// ]); +/// ``` +pub fn assert_env_vars(vars: &[(&str, &str)]) { + for (key, expected) in vars { + assert_env_var(key, expected); + } +} + +/// Assert environment variable is set and has the expected value. +pub fn assert_env_var(key: &str, expected: &str) { + match env::var(key) { + Ok(actual) => assert_eq!( + expected, actual, + "\n\nFor Environment Variable `{}`:\n EXPECTED: `{}`\n ACTUAL: `{}`\n", + key, expected, actual + ), + Err(VarError::NotPresent) => panic!("env var `{}` not found", key), + Err(VarError::NotUnicode(val)) => panic!( + "env var `{}` currently has invalid unicode: `{}`", + key, + val.to_string_lossy() + ), + } +} + +/// Assert environment variable is not currently set. +pub fn assert_env_var_unset(key: &str) { + match env::var(key) { + Ok(actual) => panic!( + "env var `{}` should not be set, currently it is: `{}`", + key, actual + ), + Err(VarError::NotUnicode(val)) => panic!( + "env var `{}` should not be set, currently has invalid unicode: `{}`", + key, + val.to_string_lossy() + ), + _ => (), + } +} + +/// Assert default testing environment variables are not set. +pub fn assert_default_keys_unset() { + assert_env_var_unset(DEFAULT_EXISTING_KEY); + assert_env_var_unset(DEFAULT_TEST_KEY); +} + +/// Assert default testing environment variables are set. +/// Assuming the default envfile is loaded. +pub fn assert_default_keys() { + assert_env_var(DEFAULT_TEST_KEY, DEFAULT_TEST_VALUE); + assert_default_existing_var(); +} + +/// Assert default existing environment variable is set. +pub fn assert_default_existing_var() { + assert_env_var(DEFAULT_EXISTING_KEY, DEFAULT_EXISTING_VALUE); +} diff --git a/test_util/src/envfile.rs b/test_util/src/envfile.rs new file mode 100644 index 00000000..4d09d6ab --- /dev/null +++ b/test_util/src/envfile.rs @@ -0,0 +1,160 @@ +use super::*; + +/// Create the default envfile contents. +/// +/// [`DEFAULT_TEST_KEY`] set as [`DEFAULT_TEST_VALUE`] +/// +/// [`DEFAULT_EXISTING_KEY`] set as [`DEFAULT_OVERRIDING_VALUE`] +#[inline(always)] +pub fn create_default_envfile() -> String { + format!( + "{}={}\n{}={}", + DEFAULT_TEST_KEY, DEFAULT_TEST_VALUE, DEFAULT_EXISTING_KEY, DEFAULT_OVERRIDING_VALUE + ) +} + +/// Invalid due to missing `=` between key and value. +#[inline(always)] +pub fn create_invalid_envfile() -> String { + format!( + "{}{}\n{}{}", + DEFAULT_TEST_KEY, DEFAULT_TEST_VALUE, DEFAULT_EXISTING_KEY, DEFAULT_OVERRIDING_VALUE + ) +} + +/// Create an envfile with custom key-value pairs. +/// +/// ## Example +/// +/// ```no_run +/// # use dotenvy_test_util::create_custom_envfile; +/// let contents = create_custom_envfile(&[ +/// ("CUSTOM_KEY", "test_val"), +/// ("ANOTHER_KEY", "another_val"), +/// ]); +/// assert_eq!(contents, "CUSTOM_KEY=test_val\nANOTHER_KEY=another_val\n"); +/// ``` +pub fn create_custom_envfile(env_vars: &[(&str, &str)]) -> String { + let mut efb = EnvFileBuilder::new(); + efb.add_vars(env_vars); + efb.into_owned_string() +} + +/// Advanced test-envfile constructor. +/// +/// Represented as bytes to allow for advanced manipulation and BOM testing. +#[derive(Debug, Default)] +pub struct EnvFileBuilder { + contents: Vec, +} + +impl EnvFileBuilder { + pub fn new() -> Self { + Self { + contents: Vec::new(), + } + } + + /// Build a byte vector from the contents of the builder. + pub fn build(&self) -> Vec { + self.contents.clone() + } + + /// Build a string from the contents of the builder. + /// + /// ## Panics + /// + /// If the contents of the builder is not valid UTF-8. + pub fn build_string(&self) -> String { + String::from_utf8(self.contents.clone()).expect("valid UTF-8") + } + + /// Transform the builder into a byte vector. + pub fn into_owned_bytes(self) -> Vec { + self.contents + } + + /// Transform the builder into a string. + /// + /// ## Panics + /// + /// If the contents of the builder is not valid UTF-8. + pub fn into_owned_string(self) -> String { + String::from_utf8(self.contents).expect("valid UTF-8") + } + + /// Get a reference to the contents of the builder. + pub fn as_bytes(&self) -> &[u8] { + &self.contents + } + + /// Add a slice of key-value pairs, separated by newlines. + /// + /// Includes a trailing newline. + pub fn add_vars(&mut self, env_vars: &[(&str, &str)]) -> &mut Self { + let mut many = String::new(); + for (key, value) in env_vars { + many.push_str(key); + many.push('='); + many.push_str(value); + many.push('\n'); + } + self.add_str(&many); + self + } + + /// Add a key-value pair and newline + pub fn add_key_value(&mut self, key: &str, value: &str) -> &mut Self { + self.add_strln(&format!("{}={}", key, value)) + } + + /// Add a string without a newline + pub fn add_str(&mut self, s: &str) -> &mut Self { + self.add_bytes(s.as_bytes()) + } + + /// Add a string with a newline + pub fn add_strln(&mut self, line: &str) -> &mut Self { + self.add_str(line).add_byte(b'\n') + } + + /// Add a byte slice + pub fn add_bytes(&mut self, bytes: &[u8]) -> &mut Self { + self.contents.extend_from_slice(bytes); + self + } + + /// Add a single byte + pub fn add_byte(&mut self, byte: u8) -> &mut Self { + self.contents.push(byte); + self + } + + /// Insert the UTF-8 Byte Order Mark at the beginning of the file + pub fn insert_utf8_bom(&mut self) -> &mut Self { + // https://www.compart.com/en/unicode/U+FEFF + let bom = b"\xEF\xBB\xBF"; + self.contents.splice(0..0, bom.iter().cloned()); + self + } +} + +impl From for Vec { + fn from(builder: EnvFileBuilder) -> Self { + builder.into_owned_bytes() + } +} + +impl From> for EnvFileBuilder { + fn from(contents: Vec) -> Self { + Self { contents } + } +} + +impl From for EnvFileBuilder { + fn from(contents: String) -> Self { + Self { + contents: contents.into_bytes(), + } + } +} diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs new file mode 100644 index 00000000..ea775d0b --- /dev/null +++ b/test_util/src/lib.rs @@ -0,0 +1,74 @@ +//! Test environment setup, assertions and helpers. +//! +//! Setup a [`TestEnv`] and run your tests via [`test_in_env`]. The environment +//! can be tweaked with: +//! +//! - pre-existing environment variables, +//! - different directory layouts, +//! - custom `.env` file contents, +//! - multiple `.env` files, +//! - custom envfile name/path. +//! +//! Use the `create_` helper functions, such as [`create_custom_envfile`], to +//! generate the `.env` file contents. If you need more control of the +//! envfile's bytes, use the [`EnvFileBuilder`]. +//! +//! In your tests, call the [`dotenvy`] API, then make use of the `assert_` +//! helpers, such as [`assert_env_var`] and [`assert_env_var_unset`], to check +//! the state of the environment. +//! +//! ## Example +//! +//! ```no_run +//! use dotenvy_test_util::*; +//! use dotenvy::dotenv_override; +//! +//! const EXISTING_KEY: &str = "TEST_KEY"; +//! const EXISTING_VAL: &str = "test_val"; +//! const OVERRIDING_VAL: &str = "overriding_val"; +//! +//! #[test] +//! fn dotenv_override_existing_key() { +//! // setup testing environment +//! let mut testenv = TestEnv::init(); +//! +//! // with an existing environment variable +//! testenv.add_env_var(EXISTING_KEY, EXISTING_VAL); +//! +//! // with an envfile that overrides it +//! testenv.add_envfile( +//! ".env", +//! create_custom_envfile(&[(EXISTING_KEY, OVERRIDING_VAL)]), +//! ); +//! +//! // execute a closure in the testing environment +//! test_in_env(&testenv, || { +//! dotenv_override().expect(".env should be loaded"); +//! assert_env_var(EXISTING_KEY, OVERRIDING_VAL); +//! }); +//! // any changes to environment variables will be reset for other tests +//! } +//! ``` + +mod assertions; +mod envfile; +mod testenv; + +#[cfg(test)] +mod tests; + +pub use assertions::*; +pub use envfile::*; +pub use testenv::*; + +/// Default key used in envfile +pub const DEFAULT_TEST_KEY: &str = "DEFAULT_TEST_KEY"; +/// Default value used in envfile +pub const DEFAULT_TEST_VALUE: &str = "default_test_val"; + +/// Default existing key set before test is run +pub const DEFAULT_EXISTING_KEY: &str = "DEFAULT_EXISTING_KEY"; +/// Default existing value set before test is run +pub const DEFAULT_EXISTING_VALUE: &str = "loaded_from_env"; +/// Default overriding value in envfile +pub const DEFAULT_OVERRIDING_VALUE: &str = "loaded_from_file"; diff --git a/test_util/src/testenv.rs b/test_util/src/testenv.rs new file mode 100644 index 00000000..c51ddd20 --- /dev/null +++ b/test_util/src/testenv.rs @@ -0,0 +1,335 @@ +use super::{create_default_envfile, DEFAULT_EXISTING_KEY, DEFAULT_EXISTING_VALUE}; +use once_cell::sync::OnceCell; +use std::{ + collections::HashMap, + env, fs, + io::{self, Write}, + path::{Path, PathBuf}, + sync::{Arc, Mutex, PoisonError}, +}; +use tempfile::{tempdir, TempDir}; + +/// Env var convenience type. +type EnvMap = HashMap; + +/// Initialized in [`get_env_locker`] +static ENV_LOCKER: OnceCell>> = OnceCell::new(); + +/// A test environment. +/// +/// Will create a new temporary directory. Use its builder methods to configure +/// the directory structure, preset variables, envfile name and contents, and +/// the working directory to run the test from. +/// +/// Creation methods: +/// - [`TestEnv::init`]: blank environment (no envfile) +/// - [`TestEnv::init_with_envfile`]: blank environment with a custom `.env` +/// - [`TestEnv::default`]: default testing environment (1 existing var and 2 +/// set in a `.env` file) +#[derive(Debug)] +pub struct TestEnv { + // Temporary directory that will be deleted on drop + _temp_dir: TempDir, + dir_path: PathBuf, + work_dir: PathBuf, + env_vars: EnvMap, + envfiles: Vec, +} + +#[derive(Debug, Clone)] +/// Simple path and byte contents representing a `.env` file +pub struct EnvFile { + pub path: PathBuf, + pub contents: Vec, +} + +/// Run a test closure within a test environment. +/// +/// Resets the environment variables, loads the [`TestEnv`], then runs the test +/// closure. Ensures only one thread has access to the process environment. +pub fn test_in_env(testenv: &TestEnv, test: F) +where + F: FnOnce(), +{ + let locker = get_env_locker(); + // ignore a poisoned mutex + // we expect some tests may panic to indicate a failure + let original_env = locker.lock().unwrap_or_else(PoisonError::into_inner); + // we reset the environment anyway upon acquiring the lock + reset_env(&original_env); + create_env(testenv); + test(); + // drop the lock +} + +/// Run a test closure within the default test environment. +/// +/// Resets the environment variables, creates the default [`TestEnv`], then runs +/// the test closure. Ensures only one thread has access to the process +/// environment. +/// +/// The default testing environment sets an existing environment variable +/// `DEFAULT_EXISTING_KEY`, which is set to `loaded_from_env`. It also creates a +/// `.env` file with the two lines: +/// +/// ```ini +/// DEFAULT_TEST_KEY=default_test_val +/// DEFAULT_EXISTING_KEY=loaded_from_file +/// ``` +/// +/// Notice that file has the potential to override `DEFAULT_EXISTING_KEY` depending +/// on the what's being tested. +pub fn test_in_default_env(test: F) +where + F: FnOnce(), +{ + let testenv = TestEnv::default(); + test_in_env(&testenv, test); +} + +/// Create a [`TestEnv`] without an envfile, but with the +/// default existing environment variable. +pub fn create_testenv_with_default_var() -> TestEnv { + let mut testenv = TestEnv::init(); + testenv.add_env_var(DEFAULT_EXISTING_KEY, DEFAULT_EXISTING_VALUE); + testenv +} + +impl TestEnv { + /// Blank testing environment in a new temporary directory. + /// + /// No envfile or pre-existing variables set. The working directory is the + /// created temporary directory. + pub fn init() -> Self { + let tempdir = tempdir().expect("create tempdir"); + let dir_path = tempdir + .path() + .canonicalize() + .expect("canonicalize dir_path"); + Self { + _temp_dir: tempdir, + work_dir: dir_path.clone(), + dir_path, + env_vars: Default::default(), + envfiles: vec![], + } + } + + /// Testing environment with custom envfile contents. + /// + /// No pre-existing env_vars set. The envfile path is set to `.env`. The + /// working directory is the created temporary directory. + pub fn init_with_envfile(contents: impl Into>) -> Self { + let mut testenv = Self::init(); + testenv.add_envfile(".env", contents); + testenv + } + + /// Add an individual envfile. + /// + /// ## Arguments + /// + /// - `path`: relative from the temporary directory + /// - `contents`: bytes or string + /// + /// ## Panics + /// + /// - if the path is empty or the same as the temporary directory + /// - if the envfile already exists + pub fn add_envfile(&mut self, path: P, contents: C) -> &mut Self + where + P: AsRef, + C: Into>, + { + let path = self.dir_path.join(path); + self.assert_envfile_path_is_valid(&path); + self.add_envfile_assume_valid(path, contents.into()) + } + + /// Add an individual environment variable. + /// + /// This adds more pre-existing environment variables to the process before + /// any tests are run. + /// + /// ## Panics + /// + /// - if the env var already exists in the testenv + /// - if the key is empty + pub fn add_env_var(&mut self, key: K, value: V) -> &mut Self + where + K: Into, + V: Into, + { + let key = key.into(); + self.assert_env_var_is_valid(&key); + self.env_vars.insert(key, value.into()); + self + } + + /// Set all the pre-existing environment variables. + /// + /// These variables will get added to the process' environment before the + /// test is run. This overrides any previous env vars added to the + /// [`TestEnv`]. + /// + /// ## Panics + /// + /// - if an env var is set twice + /// - if a key is empty + pub fn set_env_vars(&mut self, env_vars: &[(&str, &str)]) -> &mut Self { + for &(key, value) in env_vars { + self.add_env_var(key, value); + } + self + } + + /// Set the working directory the test will run from. + /// + /// The default is the created temporary directory. This method is useful if + /// you wish to run a test from a subdirectory or somewhere else. + /// + /// ## Arguments + /// + /// - `path`: relative from the temporary directory + /// + /// ## Panics + /// + /// - if the path does not exist + pub fn set_work_dir(&mut self, path: impl AsRef) -> &mut Self { + self.work_dir = self + .temp_path() + .join(path.as_ref()) + .canonicalize() + .expect("canonicalize work_dir"); + if !self.work_dir.exists() { + panic!("work_dir does not exist: {}", self.work_dir.display()); + } + self + } + + /// Create a child folder within the temporary directory. + /// + /// This will not change the working directory the test is run in, or where + /// the envfile is created. + /// + /// Will create parent directories if they are missing. + pub fn add_child_dir(&mut self, path: impl AsRef) -> &mut Self { + let path = path.as_ref(); + let child_dir = self.temp_path().join(path); + if let Err(err) = fs::create_dir_all(child_dir) { + panic!( + "unable to create child directory: `{}` in `{}`: {}", + path.display(), + self.temp_path().display(), + err + ); + } + self + } + + /// Reference to the path of the temporary directory. + pub fn temp_path(&self) -> &Path { + &self.dir_path + } + + /// Reference to the working directory the test will be run from. + pub fn work_dir(&self) -> &Path { + &self.work_dir + } + + /// Reference to environment variables that will be set **before** the test. + pub fn env_vars(&self) -> &EnvMap { + &self.env_vars + } + + /// Get a reference to the environment files that will created + pub fn envfiles(&self) -> &[EnvFile] { + &self.envfiles + } + + fn add_envfile_assume_valid(&mut self, path: PathBuf, contents: Vec) -> &mut Self { + let envfile = EnvFile { path, contents }; + self.envfiles.push(envfile); + self + } + + fn assert_envfile_path_is_valid(&self, path: &Path) { + if path == self.temp_path() { + panic!("path cannot be empty or the same as the temporary directory"); + } + if self.envfiles.iter().any(|f| f.path == path) { + panic!("envfile already in testenv: {}", path.display()); + } + } + + fn assert_env_var_is_valid(&self, key: &str) { + if key.is_empty() { + panic!("key cannot be empty"); + } + if self.env_vars.contains_key(key) { + panic!("key already in testenv: {}", key); + } + } +} + +impl Default for TestEnv { + fn default() -> Self { + let mut testenv = TestEnv::init(); + testenv.add_env_var(DEFAULT_EXISTING_KEY, DEFAULT_EXISTING_VALUE); + testenv.add_envfile(".env", create_default_envfile()); + testenv + } +} + +/// Get a guarded copy of the original process' env vars. +fn get_env_locker() -> Arc> { + Arc::clone(ENV_LOCKER.get_or_init(|| { + let map: EnvMap = env::vars().collect(); + Arc::new(Mutex::new(map)) + })) +} + +/// Reset the process' env vars back to what was in `original_env`. +fn reset_env(original_env: &EnvMap) { + // remove keys if they weren't in the original environment + env::vars() + .filter(|(key, _)| !original_env.contains_key(key)) + .for_each(|(key, _)| env::remove_var(key)); + // ensure original keys have their original values + original_env + .iter() + .for_each(|(key, value)| env::set_var(key, value)); +} + +/// Create an environment to run tests in. +/// +/// Writes the envfiles, sets the working directory, and sets environment vars. +fn create_env(testenv: &TestEnv) { + env::set_current_dir(&testenv.work_dir).expect("setting working directory"); + + for EnvFile { path, contents } in &testenv.envfiles { + create_envfile(path, contents); + } + + for (key, value) in &testenv.env_vars { + env::set_var(key, value) + } +} + +/// Create an envfile for use in tests. +fn create_envfile(path: &Path, contents: &[u8]) { + if path.exists() { + panic!("envfile `{}` already exists", path.display()) + } + // inner function to group together io::Results + fn create_env_file_inner(path: &Path, contents: &[u8]) -> io::Result<()> { + let mut file = fs::File::create(path)?; + file.write_all(contents)?; + file.sync_all() + } + // call inner function + if let Err(err) = create_env_file_inner(path, contents) { + // handle any io::Result::Err + panic!("error creating envfile `{}`: {}", path.display(), err); + } +} diff --git a/test_util/src/tests/default_env.rs b/test_util/src/tests/default_env.rs new file mode 100644 index 00000000..c2de3f89 --- /dev/null +++ b/test_util/src/tests/default_env.rs @@ -0,0 +1,34 @@ +use super::*; + +#[test] +fn vars_state() { + test_in_default_env(|| { + assert_env_var_unset(DEFAULT_TEST_KEY); + assert_default_existing_var(); + }); +} + +#[test] +fn envfile_exists() { + let testenv = TestEnv::default(); + assert_envfiles_in_testenv(&testenv); +} + +#[test] +fn envfile_loaded_vars_state() { + test_in_default_env(|| { + dotenvy::dotenv().expect(DOTENV_EXPECT); + // dotenv() does not override existing var + assert_default_keys(); + }); +} + +#[test] +fn only_default_existing() { + let testenv = create_testenv_with_default_var(); + let envfile_path = testenv.temp_path().join(".env"); + test_in_env(&testenv, || { + assert_default_existing_var(); + assert!(!envfile_path.exists()); + }); +} diff --git a/test_util/src/tests/envfile.rs b/test_util/src/tests/envfile.rs new file mode 100644 index 00000000..45239482 --- /dev/null +++ b/test_util/src/tests/envfile.rs @@ -0,0 +1,37 @@ +use super::*; + +#[test] +fn create_default() { + let expected = format!( + "{}={}\n{}={}", + DEFAULT_TEST_KEY, DEFAULT_TEST_VALUE, DEFAULT_EXISTING_KEY, DEFAULT_OVERRIDING_VALUE + ); + let actual = create_default_envfile(); + assert_eq!( + expected, actual, + "envfile should be created with default values" + ); +} + +#[test] +fn create_invalid() { + let expected = format!( + "{}{}\n{}{}", + DEFAULT_TEST_KEY, DEFAULT_TEST_VALUE, DEFAULT_EXISTING_KEY, DEFAULT_OVERRIDING_VALUE + ); + let actual = create_invalid_envfile(); + assert_eq!( + expected, actual, + "envfile should be created without equals sign" + ); +} + +#[test] +fn create_custom() { + let expected = expected_envfile(CUSTOM_VARS); + let actual = create_custom_envfile(CUSTOM_VARS); + assert_eq!( + expected, actual, + "envfile should be created with custom values" + ); +} diff --git a/test_util/src/tests/envfile_builder.rs b/test_util/src/tests/envfile_builder.rs new file mode 100644 index 00000000..cb3dc84a --- /dev/null +++ b/test_util/src/tests/envfile_builder.rs @@ -0,0 +1,117 @@ +use super::*; + +#[test] +fn new_builds_empty() { + let efb = EnvFileBuilder::new(); + assert_contents_empty(efb); +} + +#[test] +fn default_builds_empty() { + let efb = EnvFileBuilder::default(); + assert_contents_empty(efb); +} + +#[test] +fn add_key_empty_value() { + let mut efb = EnvFileBuilder::new(); + efb.add_key_value(DEFAULT_TEST_KEY, ""); + let expected = format!("{}=\n", DEFAULT_TEST_KEY); + assert_contents_str(efb, &expected); +} + +#[test] +fn add_key_value() { + let mut efb = EnvFileBuilder::new(); + efb.add_key_value(DEFAULT_TEST_KEY, DEFAULT_TEST_VALUE); + let expected = format!("{}={}\n", DEFAULT_TEST_KEY, DEFAULT_TEST_VALUE); + assert_contents_str(efb, &expected); +} + +#[test] +fn add_multiple_key_values() { + let mut efb = EnvFileBuilder::new(); + efb.add_key_value(DEFAULT_TEST_KEY, DEFAULT_TEST_VALUE); + efb.add_key_value(DEFAULT_EXISTING_KEY, DEFAULT_OVERRIDING_VALUE); + let expected = expected_envfile(&[ + (DEFAULT_TEST_KEY, DEFAULT_TEST_VALUE), + (DEFAULT_EXISTING_KEY, DEFAULT_OVERRIDING_VALUE), + ]); + assert_contents_str(efb, &expected); +} + +#[test] +fn add_vars() { + let mut efb = EnvFileBuilder::new(); + efb.add_vars(CUSTOM_VARS); + let expected = expected_envfile(CUSTOM_VARS); + assert_contents_str(efb, &expected); +} + +#[test] +fn add_str() { + let mut efb = EnvFileBuilder::new(); + efb.add_str("test"); + assert_contents_str(efb, "test"); +} + +#[test] +fn add_bytes() { + let mut efb = EnvFileBuilder::new(); + efb.add_bytes(b"test"); + assert_contents_str(efb, "test"); +} + +#[test] +fn add_byte() { + let mut efb = EnvFileBuilder::new(); + efb.add_byte(b't'); + assert_contents_str(efb, "t"); +} + +#[test] +fn insert_utf8_bom() { + let mut efb = EnvFileBuilder::new(); + efb.add_str("test"); + efb.insert_utf8_bom(); + assert_contents_str(efb, "\u{FEFF}test"); +} + +#[test] +fn add_strln() { + let mut efb = EnvFileBuilder::new(); + efb.add_strln("test"); + assert_contents_str(efb, "test\n"); +} + +#[test] +fn from_vec_u8() { + let vec: Vec = Vec::from(create_default_envfile()); + let efb = EnvFileBuilder::from(vec); + assert_contents_str(efb, &create_default_envfile()); +} + +#[test] +fn to_vec_u8() { + let mut efb = EnvFileBuilder::new(); + efb.add_str(create_default_envfile().as_str()); + let vec = Vec::from(efb); + let expected = create_default_envfile().into_bytes(); + assert_eq!(expected, vec); +} + +#[test] +fn from_string() { + let efb = EnvFileBuilder::from(create_default_envfile()); + assert_contents_str(efb, &create_default_envfile()); +} + +fn assert_contents_empty(efb: EnvFileBuilder) { + let contents = efb.into_owned_bytes(); + assert!(contents.is_empty()); +} + +fn assert_contents_str(efb: EnvFileBuilder, expected: &str) { + let contents = efb.into_owned_string(); + assert_eq!(expected, contents,); +} diff --git a/test_util/src/tests/mod.rs b/test_util/src/tests/mod.rs new file mode 100644 index 00000000..55921981 --- /dev/null +++ b/test_util/src/tests/mod.rs @@ -0,0 +1,63 @@ +use std::path::Path; + +use super::*; + +mod default_env; +mod envfile; +mod envfile_builder; +mod testenv; + +const CUSTOM_VARS: &[(&str, &str)] = &[ + ("CUSTOM_KEY_1", "CUSTOM_VALUE_1"), + ("CUSTOM_KEY_2", "CUSTOM_VALUE_2"), +]; + +const DOTENV_EXPECT: &str = "TestEnv should have .env file"; + +fn assert_envfiles_in_testenv(testenv: &TestEnv) { + let files = testenv.envfiles(); + + test_in_env(testenv, || { + for EnvFile { path, contents } in files { + assert_envfile(path, contents); + } + }); +} + +fn assert_envfile(path: &Path, expected: &[u8]) { + assert!(path.exists(), "{} should exist in testenv", path.display()); + + let actual = std::fs::read(path) + .unwrap_or_else(|e| panic!("failed to read {} in testenv: {}", path.display(), e)); + + assert_eq!( + expected, + &actual, + "{} has incorrect contents", + path.display() + ); +} + +fn assert_default_keys_not_set_in_testenv(testenv: &TestEnv) { + test_in_env(testenv, assert_default_keys_unset); +} + +fn assert_env_vars_in_testenv(testenv: &TestEnv, vars: &[(&str, &str)]) { + test_in_env(testenv, || assert_env_vars(vars)); +} + +fn assert_path_exists_in_testenv(testenv: &TestEnv, path: impl AsRef) { + let path = testenv.temp_path().join(path.as_ref()); + assert!(path.exists(), "{} should exist in testenv", path.display()); +} + +fn expected_envfile(env_vars: &[(&str, &str)]) -> String { + let mut envfile = String::new(); + for (key, value) in env_vars { + envfile.push_str(key); + envfile.push('='); + envfile.push_str(value); + envfile.push('\n'); + } + envfile +} diff --git a/test_util/src/tests/testenv.rs b/test_util/src/tests/testenv.rs new file mode 100644 index 00000000..cc4e3cd8 --- /dev/null +++ b/test_util/src/tests/testenv.rs @@ -0,0 +1,371 @@ +use super::*; +use dotenvy::dotenv; + +mod init { + use super::*; + + #[test] + fn vars_state() { + let init_testenv = TestEnv::init(); + assert_default_keys_not_set_in_testenv(&init_testenv); + } + + #[test] + fn no_envfile() { + let init_testenv = TestEnv::init(); + let envfile_path = init_testenv.temp_path().join(".env"); + + test_in_env(&init_testenv, || { + assert!(!envfile_path.exists()); + assert!(dotenv().is_err()); + }); + } + + #[test] + fn work_dir_is_temp() { + let testenv = TestEnv::init(); + assert_eq!(testenv.work_dir(), testenv.temp_path()); + } + + #[test] + fn env_vars_are_empty() { + let testenv = TestEnv::init(); + assert!(testenv.env_vars().is_empty()); + } + + #[test] + fn envfiles_are_empty() { + let testenv = TestEnv::init(); + assert!(testenv.envfiles().is_empty()); + } +} + +mod init_with_envfile { + use super::*; + + #[test] + fn default_envfile_vars_state() { + let testenv = init_default_envfile_testenv(); + assert_default_keys_not_set_in_testenv(&testenv); + } + + #[test] + fn default_envfile_exists() { + let testenv = init_default_envfile_testenv(); + assert_envfiles_in_testenv(&testenv); + } + + #[test] + fn default_envfile_loaded_vars_state() { + let testenv = init_default_envfile_testenv(); + test_in_env(&testenv, || { + dotenv().expect(DOTENV_EXPECT); + // dotenv() does not override existing var + // but existing key is not set in this testenv + assert_env_var(DEFAULT_EXISTING_KEY, DEFAULT_OVERRIDING_VALUE); + assert_env_var(DEFAULT_TEST_KEY, DEFAULT_TEST_VALUE); + }); + } + + #[test] + fn custom_envfile_vars_state() { + let testenv = init_custom_envfile_testenv(); + test_in_env(&testenv, || { + assert_default_keys_unset(); + for (key, _) in CUSTOM_VARS { + assert_env_var_unset(key); + } + }); + } + + #[test] + fn custom_envfile_exists() { + let testenv = init_custom_envfile_testenv(); + assert_envfiles_in_testenv(&testenv); + } + + #[test] + fn custom_envfile_loaded_vars_state() { + let testenv = init_custom_envfile_testenv(); + test_in_env(&testenv, || { + dotenv().expect(DOTENV_EXPECT); + assert_default_keys_unset(); + assert_env_vars(CUSTOM_VARS); + }); + } + + #[test] + fn empty_envfile_exists() { + let testenv = init_empty_envfile_testenv(); + assert_envfiles_in_testenv(&testenv); + } + + #[test] + fn empty_envfile_loaded_vars_state() { + let testenv = init_empty_envfile_testenv(); + test_in_env(&testenv, || { + dotenv().expect(DOTENV_EXPECT); + assert_default_keys_unset(); + }); + } + + #[test] + fn custom_bom_envfile_exists() { + let testenv = init_custom_bom_envfile_testenv(); + assert_envfiles_in_testenv(&testenv); + } + + #[test] + fn custom_bom_envfile_loaded_vars_state() { + let testenv = init_custom_bom_envfile_testenv(); + test_in_env(&testenv, || { + dotenv().expect(DOTENV_EXPECT); + assert_env_var(DEFAULT_TEST_KEY, DEFAULT_TEST_VALUE); + }); + } + + fn init_default_envfile_testenv() -> TestEnv { + let envfile = create_default_envfile(); + TestEnv::init_with_envfile(envfile) + } + + fn init_custom_envfile_testenv() -> TestEnv { + let envfile = create_custom_envfile(CUSTOM_VARS); + TestEnv::init_with_envfile(envfile) + } + + fn init_empty_envfile_testenv() -> TestEnv { + TestEnv::init_with_envfile([]) + } + + fn init_custom_bom_envfile_testenv() -> TestEnv { + let mut efb = EnvFileBuilder::new(); + efb.add_key_value(DEFAULT_TEST_KEY, DEFAULT_TEST_VALUE); + efb.insert_utf8_bom(); + let envfile = efb.into_owned_string(); + TestEnv::init_with_envfile(envfile) + } +} + +mod add_envfile { + use super::*; + + #[test] + #[should_panic] + fn panics_add_twice() { + let mut testenv = TestEnv::init(); + testenv.add_envfile(".env", create_default_envfile()); + testenv.add_envfile(".env", create_custom_envfile(CUSTOM_VARS)); + } + + #[test] + #[should_panic] + fn panics_same_path_as_init() { + let mut testenv = TestEnv::init_with_envfile(create_default_envfile()); + testenv.add_envfile(".env", create_default_envfile()); + } + + #[test] + #[should_panic] + fn panics_same_path_as_default() { + let mut testenv = TestEnv::default(); + testenv.add_envfile(".env", create_invalid_envfile()); + } + + #[test] + #[should_panic] + fn panics_path() { + let mut testenv = TestEnv::init(); + testenv.add_envfile("", create_default_envfile()); + } + + #[test] + fn allow_empty_contents() { + let mut testenv = TestEnv::init(); + testenv.add_envfile(".env", []); + assert_envfiles_in_testenv(&testenv); + } + + #[test] + fn allow_absolute_path() { + let mut testenv = TestEnv::init(); + let path = testenv.temp_path().join(".env"); + assert!(path.is_absolute()); + testenv.add_envfile(&path, create_default_envfile()); + assert_envfiles_in_testenv(&testenv); + } + + #[test] + fn two_files() { + let mut testenv = TestEnv::init(); + testenv.add_envfile(".env", create_default_envfile()); + testenv.add_envfile(".env.local", create_custom_envfile(CUSTOM_VARS)); + assert_envfiles_in_testenv(&testenv); + } +} + +mod add_env_var { + use super::*; + + #[test] + #[should_panic] + fn panics_add_twice() { + let mut testenv = TestEnv::init(); + testenv.add_env_var("TEST_KEY", "one_value"); + testenv.add_env_var("TEST_KEY", "two_value"); + } + + #[test] + #[should_panic] + fn panics_same_var_as_default() { + let mut testenv = TestEnv::default(); + testenv.add_env_var(DEFAULT_EXISTING_KEY, "value"); + } + + #[test] + #[should_panic] + fn panics_emtpy_key() { + let mut testenv = TestEnv::init(); + testenv.add_env_var("", "value"); + } + + #[test] + fn allow_empty_value() { + let mut testenv = TestEnv::init(); + testenv.add_env_var("TEST_KEY", ""); + assert_env_vars_in_testenv(&testenv, &[("TEST_KEY", "")]); + } + + #[test] + fn two_vars() { + let mut testenv = TestEnv::init(); + let vars = [("TEST_KEY", "one_value"), ("TEST_KEY_2", "two_value")]; + testenv.add_env_var(vars[0].0, vars[0].1); + testenv.add_env_var(vars[1].0, vars[1].1); + assert_env_vars_in_testenv(&testenv, &vars); + } + + #[test] + fn owned_strings() { + let mut testenv = TestEnv::init(); + testenv.add_env_var("TEST_KEY".to_string(), "test_val".to_string()); + assert_env_vars_in_testenv(&testenv, &[("TEST_KEY", "test_val")]); + } +} + +mod set_env_vars { + use super::*; + + #[test] + #[should_panic] + fn panics_double_key() { + let mut testenv = TestEnv::init(); + let mut vars = VARS.to_vec(); + vars.push(VARS[0]); + testenv.set_env_vars(&vars); + } + + #[test] + #[should_panic] + fn panics_empty_key() { + let mut testenv = TestEnv::init(); + testenv.set_env_vars(&[("", "value")]); + } + + #[test] + fn from_tuples_slice() { + let mut testenv = TestEnv::init(); + testenv.set_env_vars(VARS.as_slice()); + assert_vars_in_testenv(&testenv); + } + + #[test] + fn from_tuples_ref() { + let mut testenv = TestEnv::init(); + testenv.set_env_vars(&VARS); + assert_vars_in_testenv(&testenv); + } + + #[test] + fn from_vec_slice() { + let mut testenv = TestEnv::init(); + let vec = VARS.to_vec(); + testenv.set_env_vars(vec.as_slice()); + assert_vars_in_testenv(&testenv); + } + + const VARS: [(&str, &str); 2] = [("TEST_KEY", "one_value"), ("TEST_KEY_2", "two_value")]; + + fn assert_vars_in_testenv(testenv: &TestEnv) { + assert_env_vars_in_testenv(testenv, &VARS); + } +} + +mod set_work_dir { + use super::*; + + #[test] + #[should_panic] + fn panics_non_existing() { + let mut testenv = TestEnv::init(); + testenv.set_work_dir("subdir"); + } + + #[test] + fn allow_absolute_path() { + let mut testenv = TestEnv::init(); + let path = testenv.temp_path().join("subdir"); + assert!(path.is_absolute()); + std::fs::create_dir_all(&path).expect("failed to create subdir"); + testenv.set_work_dir(&path); + assert_path_exists_in_testenv(&testenv, "subdir"); + } + + #[test] + fn relative_path() { + let mut testenv = TestEnv::init(); + std::fs::create_dir_all(testenv.temp_path().join("subdir")) + .expect("failed to create subdir"); + testenv.set_work_dir("subdir"); + assert_path_exists_in_testenv(&testenv, "subdir"); + } + + #[test] + fn in_testenv() { + let mut testenv = TestEnv::init(); + std::fs::create_dir_all(testenv.temp_path().join("subdir")) + .expect("failed to create subdir"); + testenv.set_work_dir("subdir"); + test_in_env(&testenv, || { + let current_dir = std::env::current_dir().expect("failed to get current dir"); + assert_eq!(current_dir, testenv.work_dir()); + }); + } +} + +mod add_child_dir { + use super::*; + + #[test] + fn subdir() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("subdir"); + assert_path_exists_in_testenv(&testenv, "subdir"); + } + + #[test] + fn allow_absolute_path() { + let mut testenv = TestEnv::init(); + let path = testenv.temp_path().join("subdir"); + assert!(path.is_absolute()); + testenv.add_child_dir(&path); + assert_path_exists_in_testenv(&testenv, "subdir"); + } + + #[test] + fn create_parents() { + let mut testenv = TestEnv::init(); + testenv.add_child_dir("subdir/subsubdir"); + assert_path_exists_in_testenv(&testenv, "subdir/subsubdir"); + } +}