use std::io::Write;
use std::process::{Command, Output, Stdio};
fn setup_project(dir: &std::path::Path) {
Command::new(env!("CARGO_BIN_EXE_envy"))
.arg("init")
.current_dir(dir)
.status()
.expect("envy init failed to spawn");
}
fn envy(args: &[&str], cwd: &std::path::Path) -> Output {
Command::new(env!("CARGO_BIN_EXE_envy"))
.args(args)
.current_dir(cwd)
.output()
.expect("failed to spawn envy")
}
#[test]
fn bare_invocation_is_help_only_when_stdout_is_piped() {
let output = Command::new(env!("CARGO_BIN_EXE_envy"))
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("failed to spawn bare envy");
assert_eq!(output.status.code(), Some(0));
assert!(output.stdout.is_empty());
assert!(String::from_utf8_lossy(&output.stderr).contains("Usage: envy"));
assert!(!output.stdout.windows(2).any(|bytes| bytes == [0x1b, b'[']));
}
#[test]
#[ignore = "requires a live OS keyring daemon (Secret Service / Keychain)"]
fn cli_init_creates_manifest() {
let tmp = tempfile::tempdir().expect("tempdir");
let status = Command::new(env!("CARGO_BIN_EXE_envy"))
.arg("init")
.current_dir(tmp.path())
.status()
.expect("envy init failed to spawn");
assert!(status.success(), "envy init must exit 0, got: {status}");
let manifest_path = tmp.path().join("envy.toml");
assert!(manifest_path.exists(), "envy.toml must be created by init");
let content = std::fs::read_to_string(&manifest_path).expect("read envy.toml");
assert!(
content.contains("project_id"),
"envy.toml must contain a project_id field, got:\n{content}"
);
let uuid_line = content
.lines()
.find(|l| l.contains("project_id"))
.expect("project_id line must exist");
let uuid = uuid_line
.split('"')
.nth(1)
.expect("project_id value must be quoted in TOML");
assert_eq!(
uuid.len(),
36,
"project_id must be a 36-char UUID, got: {uuid}"
);
assert_eq!(
uuid.chars().filter(|&c| c == '-').count(),
4,
"project_id UUID must contain exactly 4 hyphens, got: {uuid}"
);
}
#[test]
#[ignore = "requires a live OS keyring daemon (Secret Service / Keychain)"]
fn cli_set_and_get_round_trip() {
let tmp = tempfile::tempdir().expect("tempdir");
setup_project(tmp.path());
let set_out = envy(&["set", "API_KEY=secret123"], tmp.path());
assert!(
set_out.status.success(),
"envy set must exit 0, stderr: {}",
String::from_utf8_lossy(&set_out.stderr)
);
let get_out = envy(&["get", "API_KEY"], tmp.path());
assert!(
get_out.status.success(),
"envy get must exit 0, stderr: {}",
String::from_utf8_lossy(&get_out.stderr)
);
let stdout = String::from_utf8_lossy(&get_out.stdout);
assert_eq!(
stdout.as_ref(),
"secret123\n",
"stdout must be exactly 'secret123\\n', got: {stdout:?}"
);
}
#[test]
#[ignore = "requires a live OS keyring daemon (Secret Service / Keychain)"]
fn cli_set_stdin_trims_trailing_newline() {
let tmp = tempfile::tempdir().expect("tempdir");
setup_project(tmp.path());
let mut child = Command::new(env!("CARGO_BIN_EXE_envy"))
.args(["set", "--stdin", "API_KEY"])
.current_dir(tmp.path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn envy set --stdin");
child
.stdin
.take()
.expect("child stdin must be piped")
.write_all(b"secret123\n")
.expect("write to child stdin");
let set_out = child
.wait_with_output()
.expect("failed to wait on envy set --stdin");
assert!(
set_out.status.success(),
"envy set --stdin must exit 0, stderr: {}",
String::from_utf8_lossy(&set_out.stderr)
);
let get_out = envy(&["get", "API_KEY"], tmp.path());
assert!(
get_out.status.success(),
"envy get must exit 0, stderr: {}",
String::from_utf8_lossy(&get_out.stderr)
);
let stdout = String::from_utf8_lossy(&get_out.stdout);
assert_eq!(
stdout.as_ref(),
"secret123\n",
"stdin-piped 'secret123\\n' must round-trip to exactly 'secret123' \
(plus get's own trailing newline), got: {stdout:?}"
);
}
#[test]
#[ignore = "requires a live OS keyring daemon (Secret Service / Keychain)"]
fn cli_list_never_shows_values() {
let tmp = tempfile::tempdir().expect("tempdir");
setup_project(tmp.path());
envy(&["set", "API_KEY=secret123"], tmp.path());
let list_out = envy(&["list"], tmp.path());
assert!(
list_out.status.success(),
"envy list must exit 0, stderr: {}",
String::from_utf8_lossy(&list_out.stderr)
);
let stdout = String::from_utf8_lossy(&list_out.stdout);
assert!(
stdout.contains("API_KEY"),
"stdout must contain the key name 'API_KEY', got: {stdout:?}"
);
assert!(
!stdout.contains("secret123"),
"stdout must NOT contain the secret value, got: {stdout:?}"
);
}
#[test]
#[ignore = "requires a live OS keyring daemon (Secret Service / Keychain)"]
fn cli_rm_then_get_fails() {
let tmp = tempfile::tempdir().expect("tempdir");
setup_project(tmp.path());
envy(&["set", "DEL_KEY=val"], tmp.path());
let rm_out = envy(&["rm", "DEL_KEY"], tmp.path());
assert!(
rm_out.status.success(),
"envy rm must exit 0, stderr: {}",
String::from_utf8_lossy(&rm_out.stderr)
);
let get_out = envy(&["get", "DEL_KEY"], tmp.path());
assert!(
!get_out.status.success(),
"envy get after rm must exit non-zero (secret was deleted)"
);
}
#[test]
#[ignore = "requires a live OS keyring daemon (Secret Service / Keychain)"]
fn cli_run_injects_secrets() {
let tmp = tempfile::tempdir().expect("tempdir");
setup_project(tmp.path());
envy(&["set", "ENVY_TEST_VAR=hello"], tmp.path());
let run_out = envy(&["run", "--", "printenv", "ENVY_TEST_VAR"], tmp.path());
assert!(
run_out.status.success(),
"envy run must exit 0, stderr: {}",
String::from_utf8_lossy(&run_out.stderr)
);
let stdout = String::from_utf8_lossy(&run_out.stdout);
assert_eq!(
stdout.as_ref(),
"hello\n",
"stdout must be exactly 'hello\\n', got: {stdout:?}"
);
}
#[test]
#[ignore = "requires a live OS keyring daemon (Secret Service / Keychain)"]
fn cli_run_proxies_exit_code() {
let tmp = tempfile::tempdir().expect("tempdir");
setup_project(tmp.path());
let run_out = Command::new(env!("CARGO_BIN_EXE_envy"))
.args(["run", "--", "sh", "-c", "exit 42"])
.current_dir(tmp.path())
.status()
.expect("failed to spawn envy run");
assert_eq!(
run_out.code(),
Some(42),
"envy run must proxy the child exit code 42 exactly"
);
}
#[test]
#[ignore = "requires a live OS keyring daemon (Secret Service / Keychain)"]
fn cli_encrypt_and_enc_alias_work() {
let tmp = tempfile::tempdir().expect("tempdir");
setup_project(tmp.path());
envy(&["set", "ENCRYPT_TEST=hello"], tmp.path());
let enc_out = Command::new(env!("CARGO_BIN_EXE_envy"))
.args(["encrypt"])
.env("ENVY_PASSPHRASE", "integration-pass")
.current_dir(tmp.path())
.output()
.expect("failed to spawn envy encrypt");
assert!(
enc_out.status.success(),
"envy encrypt must exit 0, stderr: {}",
String::from_utf8_lossy(&enc_out.stderr)
);
assert!(
tmp.path().join("envy.enc").exists(),
"envy encrypt must create envy.enc"
);
std::fs::remove_file(tmp.path().join("envy.enc")).expect("remove envy.enc");
let alias_out = Command::new(env!("CARGO_BIN_EXE_envy"))
.args(["enc"])
.env("ENVY_PASSPHRASE", "integration-pass")
.current_dir(tmp.path())
.output()
.expect("failed to spawn envy enc");
assert!(
alias_out.status.success(),
"envy enc alias must exit 0, stderr: {}",
String::from_utf8_lossy(&alias_out.stderr)
);
assert!(
tmp.path().join("envy.enc").exists(),
"envy enc alias must create envy.enc"
);
}
#[test]
#[ignore = "requires a live OS keyring daemon (Secret Service / Keychain)"]
fn cli_decrypt_and_dec_alias_work() {
let tmp = tempfile::tempdir().expect("tempdir");
setup_project(tmp.path());
envy(&["set", "DECRYPT_TEST=world"], tmp.path());
let seal_out = Command::new(env!("CARGO_BIN_EXE_envy"))
.args(["encrypt"])
.env("ENVY_PASSPHRASE", "dec-test-pass")
.current_dir(tmp.path())
.output()
.expect("failed to spawn envy encrypt");
assert!(
seal_out.status.success(),
"encrypt setup must succeed, stderr: {}",
String::from_utf8_lossy(&seal_out.stderr)
);
let dec_out = Command::new(env!("CARGO_BIN_EXE_envy"))
.args(["decrypt"])
.env("ENVY_PASSPHRASE", "dec-test-pass")
.current_dir(tmp.path())
.output()
.expect("failed to spawn envy decrypt");
assert!(
dec_out.status.success(),
"envy decrypt must exit 0, stderr: {}",
String::from_utf8_lossy(&dec_out.stderr)
);
let alias_out = Command::new(env!("CARGO_BIN_EXE_envy"))
.args(["dec"])
.env("ENVY_PASSPHRASE", "dec-test-pass")
.current_dir(tmp.path())
.output()
.expect("failed to spawn envy dec");
assert!(
alias_out.status.success(),
"envy dec alias must exit 0, stderr: {}",
String::from_utf8_lossy(&alias_out.stderr)
);
}
#[test]
#[ignore = "requires a live OS keyring daemon (Secret Service / Keychain)"]
fn cli_migrate_imports_env_file() {
let tmp = tempfile::tempdir().expect("tempdir");
setup_project(tmp.path());
let env_file = tmp.path().join("legacy.env");
std::fs::write(
&env_file,
"# This is a comment\nDB_HOST=localhost\nDB_PORT=5432\n\nDB_NAME=myapp\n",
)
.expect("write legacy.env");
let migrate_out = envy(
&[
"migrate",
env_file.to_str().expect("env file path is UTF-8"),
],
tmp.path(),
);
assert!(
migrate_out.status.success(),
"envy migrate must exit 0, stderr: {}",
String::from_utf8_lossy(&migrate_out.stderr)
);
for (key, expected) in [
("DB_HOST", "localhost"),
("DB_PORT", "5432"),
("DB_NAME", "myapp"),
] {
let get_out = envy(&["get", key], tmp.path());
assert!(
get_out.status.success(),
"envy get {key} must succeed after migrate, stderr: {}",
String::from_utf8_lossy(&get_out.stderr)
);
let stdout = String::from_utf8_lossy(&get_out.stdout);
assert_eq!(
stdout.as_ref(),
format!("{expected}\n").as_str(),
"envy get {key} must return '{expected}\\n', got: {stdout:?}"
);
}
}