#[cfg(test)]
mod tests {
use crate::{config, envfile, error, project, vault};
use rand::RngCore;
use std::fs;
use std::process::Command;
use tempfile::tempdir;
use vault::SecretFormat;
fn get_test_key() -> [u8; 32] {
let mut key = [0u8; 32];
rand::rng().fill_bytes(&mut key);
key
}
#[test]
fn test_project_init_creates_expected_files() {
let dir = tempdir().unwrap();
let root = dir.path();
let result = project::init_at(root);
assert!(result.is_ok());
let cred_dir = root.join(".cred");
assert!(cred_dir.exists());
assert!(cred_dir.join("project.toml").exists());
assert!(cred_dir.join("vault.enc").exists());
}
#[test]
fn test_gitignore_update() {
let dir = tempdir().unwrap();
let root = dir.path();
let gitignore = root.join(".gitignore");
let cred_dir = root.join(".cred");
fs::create_dir(&cred_dir).unwrap();
fs::write(cred_dir.join("project.toml"), "").unwrap();
fs::write(&gitignore, "target/\n").unwrap();
let entry = "\n.cred/\n";
let mut file = fs::OpenOptions::new()
.write(true)
.append(true)
.open(&gitignore)
.unwrap();
use std::io::Write;
writeln!(file, "{}", entry).unwrap();
let content = fs::read_to_string(&gitignore).unwrap();
assert!(content.contains("target/"));
assert!(content.contains(".cred/"));
}
#[test]
fn test_git_binding_present() {
let dir = tempdir().unwrap();
let root = dir.path();
Command::new("git")
.args(["init"])
.current_dir(root)
.output()
.unwrap();
Command::new("git")
.args(["remote", "add", "origin", "git@github.com:org/repo.git"])
.current_dir(root)
.output()
.unwrap();
let result = project::init_at(root);
assert!(result.is_ok());
let cred_dir = root.join(".cred");
let proj = project::Project {
vault_path: cred_dir.join("vault.enc"),
config_path: cred_dir.join("project.toml"),
};
let cfg = proj.load_config().unwrap();
assert_eq!(cfg.git_repo, Some("org/repo".to_string()));
let expected_root = root.canonicalize().unwrap().to_string_lossy().to_string();
let actual_root = cfg.git_root.map(|p| {
std::path::Path::new(&p)
.canonicalize()
.unwrap_or_default()
.to_string_lossy()
.to_string()
});
assert_eq!(actual_root, Some(expected_root));
}
#[test]
fn test_git_binding_absent() {
let dir = tempdir().unwrap();
let root = dir.path();
let result = project::init_at(root);
assert!(result.is_ok());
let cred_dir = root.join(".cred");
let proj = project::Project {
vault_path: cred_dir.join("vault.enc"),
config_path: cred_dir.join("project.toml"),
};
let cfg = proj.load_config().unwrap();
assert!(cfg.git_repo.is_none());
assert!(cfg.git_root.is_none());
}
#[test]
fn test_global_config_logic() {
let dir = tempdir().unwrap();
let config_dir = dir.path().join("mock_config");
let path = config::ensure_config_at(&config_dir).unwrap();
assert!(path.exists());
let content = fs::read_to_string(&path).unwrap();
assert!(content.contains("[targets]"));
}
#[test]
fn test_vault_persistence() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set("API_URL_DEV", "http://dev.local");
v.set("API_URL_PROD", "https://prod.com");
v.set("DB_PASS", "secret");
v.save().unwrap();
let v2 = vault::Vault::load(&vault_path, key).unwrap();
assert_eq!(v2.get("API_URL_DEV"), Some(&"http://dev.local".to_string()));
assert_eq!(
v2.get("API_URL_PROD"),
Some(&"https://prod.com".to_string())
);
assert_eq!(v2.get("DB_PASS"), Some(&"secret".to_string()));
}
#[test]
fn test_vault_removal() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set("KEY", "VAL");
v.save().unwrap();
let removed = v.remove("KEY");
assert_eq!(removed, Some("VAL".to_string()));
assert_eq!(v.get("KEY"), None);
assert_eq!(v.remove("GHOST"), None);
}
#[test]
fn test_vault_listing() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set("A", "1");
v.set("B", "2");
let list = v.list();
assert_eq!(list.len(), 2);
assert!(list.contains_key("A"));
assert!(list.contains_key("B"));
}
#[test]
fn test_vault_encryption_actually_works() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set("SECRET_KEY", "PLAIN_TEXT_PASSWORD");
v.save().unwrap();
let raw_content = fs::read_to_string(&vault_path).unwrap();
assert!(!raw_content.contains("PLAIN_TEXT_PASSWORD"));
assert!(raw_content.contains("ciphertext"));
}
#[test]
fn test_vault_serialization_fields() {
#[derive(serde::Deserialize)]
struct EncFile {
version: u8,
nonce: String,
ciphertext: String,
}
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set("K1", "V1");
v.save().unwrap();
let raw = std::fs::read_to_string(&vault_path).unwrap();
let parsed: EncFile = serde_json::from_str(&raw).unwrap();
assert_eq!(parsed.version, 2);
assert!(!parsed.nonce.is_empty());
assert!(!parsed.ciphertext.is_empty());
}
#[test]
fn test_env_import_skips_without_overwrite() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set("EXISTING", "old");
v.save().unwrap();
let env_path = dir.path().join("sample.env");
fs::write(&env_path, "EXISTING=new\nNEW=value\n").unwrap();
let entries = envfile::parse_env_file(&env_path).unwrap();
let stats = envfile::import_entries(&entries, &mut v, false, false);
v.save().unwrap();
assert_eq!(stats.added, 1);
assert_eq!(stats.skipped, 1);
assert_eq!(stats.overwritten, 0);
let reloaded = vault::Vault::load(&vault_path, key).unwrap();
assert_eq!(reloaded.get("EXISTING"), Some(&"old".to_string()));
assert_eq!(reloaded.get("NEW"), Some(&"value".to_string()));
}
#[test]
fn test_env_import_overwrites_with_flag() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set("EXISTING", "old");
v.save().unwrap();
let env_path = dir.path().join("overwrite.env");
fs::write(&env_path, "EXISTING=new\n").unwrap();
let entries = envfile::parse_env_file(&env_path).unwrap();
let stats = envfile::import_entries(&entries, &mut v, true, false);
v.save().unwrap();
assert_eq!(stats.added, 0);
assert_eq!(stats.skipped, 0);
assert_eq!(stats.overwritten, 1);
let reloaded = vault::Vault::load(&vault_path, key).unwrap();
assert_eq!(reloaded.get("EXISTING"), Some(&"new".to_string()));
}
#[test]
fn test_env_export_guard_and_content() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set("B", "2");
v.set("A", "1");
v.save().unwrap();
let out_path = dir.path().join("env.out");
fs::write(&out_path, "OLD=1\n").unwrap();
let vault_view = vault::Vault::load(&vault_path, key).unwrap();
let err = envfile::export_env_file(&vault_view, &out_path, false, false).unwrap_err();
assert_eq!(err.code as i32, error::ExitCode::UserError as i32);
assert_eq!(fs::read_to_string(&out_path).unwrap(), "OLD=1\n");
let count = envfile::export_env_file(&vault_view, &out_path, true, false).unwrap();
assert_eq!(count, 2);
let content = fs::read_to_string(&out_path).unwrap();
assert_eq!(content, "A=1\nB=2\n");
}
#[test]
fn test_push_dry_run_diff_ordering() {
let mut map = std::collections::HashMap::new();
map.insert("C".to_string(), "3".to_string());
map.insert("A".to_string(), "1".to_string());
map.insert("B".to_string(), "2".to_string());
let mut keys: Vec<String> = map.keys().cloned().collect();
keys.sort();
assert_eq!(keys, vec!["A", "B", "C"]);
}
#[test]
fn test_keys_sorted_for_dry_run() {
let mut secrets = std::collections::HashMap::new();
secrets.insert("B".to_string(), "2".to_string());
secrets.insert("A".to_string(), "1".to_string());
secrets.insert("C".to_string(), "3".to_string());
let mut plan_keys: Vec<String> = secrets.keys().cloned().collect();
plan_keys.sort();
assert_eq!(plan_keys, vec!["A", "B", "C"]);
}
#[test]
fn test_vault_corruption_handled_gracefully() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
fs::write(&vault_path, "garbage-data").unwrap();
let key = get_test_key();
let result = vault::Vault::load(&vault_path, key);
assert!(result.is_err());
}
#[test]
fn test_secret_list_is_deterministic() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set("C", "3");
v.set("A", "1");
v.set("B", "2");
v.save().unwrap();
let mut keys: Vec<String> = v.list().keys().cloned().collect();
keys.sort();
assert_eq!(keys, vec!["A", "B", "C"]);
let list1 = v.list();
let list2 = v.list();
assert_eq!(list1, list2);
}
#[test]
fn test_format_auto_detection_pem() {
let pem_key = "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBg...\n-----END PRIVATE KEY-----";
assert_eq!(vault::Vault::detect_format(pem_key), SecretFormat::Pem);
let pem_cert =
"-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIJAJC1...\n-----END CERTIFICATE-----";
assert_eq!(vault::Vault::detect_format(pem_cert), SecretFormat::Pem);
let pem_rsa =
"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA...\n-----END RSA PRIVATE KEY-----";
assert_eq!(vault::Vault::detect_format(pem_rsa), SecretFormat::Pem);
}
#[test]
fn test_format_auto_detection_json() {
let json_obj = r#"{"api_key": "sk-xxx", "org": "acme"}"#;
assert_eq!(vault::Vault::detect_format(json_obj), SecretFormat::Json);
let json_arr = r#"["secret1", "secret2", "secret3"]"#;
assert_eq!(vault::Vault::detect_format(json_arr), SecretFormat::Json);
let fake_json = r#"{not valid json}"#;
assert_eq!(vault::Vault::detect_format(fake_json), SecretFormat::Raw);
let not_json = "{abc}";
assert_eq!(vault::Vault::detect_format(not_json), SecretFormat::Raw);
}
#[test]
fn test_format_auto_detection_base64() {
let b64 = "SGVsbG8gV29ybGQhIFRoaXMgaXMgYSB0ZXN0Lg==";
assert_eq!(vault::Vault::detect_format(b64), SecretFormat::Base64);
let short = "abc123";
assert_eq!(vault::Vault::detect_format(short), SecretFormat::Raw);
let bad_len = "SGVsbG8gV29ybGQhIFRoaXM=X";
assert_eq!(vault::Vault::detect_format(bad_len), SecretFormat::Raw);
let aws_key = "AKIAIOSFODNN7EXAMPLE";
assert_eq!(vault::Vault::detect_format(aws_key), SecretFormat::Raw);
let with_dash = "SGVsbG8tV29ybGQ=";
assert_eq!(vault::Vault::detect_format(with_dash), SecretFormat::Raw);
}
#[test]
fn test_format_auto_detection_multiline() {
let multiline = "line one\nline two\nline three";
assert_eq!(
vault::Vault::detect_format(multiline),
SecretFormat::Multiline
);
}
#[test]
fn test_format_auto_detection_raw() {
assert_eq!(
vault::Vault::detect_format("simple-api-key"),
SecretFormat::Raw
);
assert_eq!(vault::Vault::detect_format(""), SecretFormat::Raw);
assert_eq!(
vault::Vault::detect_format("sk-proj-abc123xyz"),
SecretFormat::Raw
);
}
#[test]
fn test_secret_entry_metadata_persistence() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set_with_metadata(
"API_KEY",
"sk-test-123",
SecretFormat::Raw,
Some("Production API key".to_string()),
);
v.save().unwrap();
let v2 = vault::Vault::load(&vault_path, key).unwrap();
let entry = v2.get_entry("API_KEY").expect("entry should exist");
assert_eq!(entry.value, "sk-test-123");
assert_eq!(entry.format, SecretFormat::Raw);
assert_eq!(entry.description, Some("Production API key".to_string()));
assert!(entry.hash.is_none());
}
#[test]
fn test_get_entry_vs_get() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set_with_metadata(
"KEY",
"value",
SecretFormat::Base64,
Some("desc".to_string()),
);
assert_eq!(v.get("KEY"), Some(&"value".to_string()));
let entry = v.get_entry("KEY").unwrap();
assert_eq!(entry.value, "value");
assert_eq!(entry.format, SecretFormat::Base64);
assert_eq!(entry.description, Some("desc".to_string()));
}
#[test]
fn test_list_entries_vs_list() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set_with_metadata("A", "1", SecretFormat::Raw, Some("first".to_string()));
v.set("B", "2");
let simple = v.list();
assert_eq!(simple.len(), 2);
assert_eq!(simple.get("A"), Some(&"1".to_string()));
assert_eq!(simple.get("B"), Some(&"2".to_string()));
let entries = v.list_entries();
assert_eq!(entries.len(), 2);
assert_eq!(
entries.get("A").unwrap().description,
Some("first".to_string())
);
assert!(entries.get("B").unwrap().description.is_none());
}
#[test]
fn test_set_description() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set("KEY", "value");
let original_updated = v.get_entry("KEY").unwrap().updated_at;
std::thread::sleep(std::time::Duration::from_millis(10));
assert!(v.set_description("KEY", Some("new description".to_string())));
let entry = v.get_entry("KEY").unwrap();
assert_eq!(entry.description, Some("new description".to_string()));
assert!(entry.updated_at > original_updated);
assert!(v.set_description("KEY", None));
assert!(v.get_entry("KEY").unwrap().description.is_none());
assert!(!v.set_description("GHOST", Some("desc".to_string())));
}
#[test]
fn test_remove_entry_returns_full_metadata() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set_with_metadata(
"KEY",
"secret",
SecretFormat::Multiline,
Some("my cert".to_string()),
);
let removed = v.remove_entry("KEY").expect("should return entry");
assert_eq!(removed.value, "secret");
assert_eq!(removed.format, SecretFormat::Multiline);
assert_eq!(removed.description, Some("my cert".to_string()));
assert!(v.get("KEY").is_none());
assert!(v.remove_entry("KEY").is_none());
}
#[test]
fn test_timestamps_on_update() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set("KEY", "original");
let entry1 = v.get_entry("KEY").unwrap();
let created = entry1.created_at;
let updated1 = entry1.updated_at;
std::thread::sleep(std::time::Duration::from_millis(10));
v.set("KEY", "new-value");
let entry2 = v.get_entry("KEY").unwrap();
assert_eq!(entry2.created_at, created, "created_at should not change");
assert!(entry2.updated_at > updated1, "updated_at should increase");
assert_eq!(entry2.value, "new-value");
}
#[test]
fn test_secret_format_parsing() {
assert_eq!("raw".parse::<SecretFormat>().unwrap(), SecretFormat::Raw);
assert_eq!(
"multiline".parse::<SecretFormat>().unwrap(),
SecretFormat::Multiline
);
assert_eq!("pem".parse::<SecretFormat>().unwrap(), SecretFormat::Pem);
assert_eq!("PEM".parse::<SecretFormat>().unwrap(), SecretFormat::Pem); assert_eq!(
"base64".parse::<SecretFormat>().unwrap(),
SecretFormat::Base64
);
assert_eq!("json".parse::<SecretFormat>().unwrap(), SecretFormat::Json);
assert_eq!("JSON".parse::<SecretFormat>().unwrap(), SecretFormat::Json);
assert!("invalid".parse::<SecretFormat>().is_err());
assert_eq!(SecretFormat::Raw.to_string(), "raw");
assert_eq!(SecretFormat::Multiline.to_string(), "multiline");
assert_eq!(SecretFormat::Pem.to_string(), "pem");
assert_eq!(SecretFormat::Base64.to_string(), "base64");
assert_eq!(SecretFormat::Json.to_string(), "json");
}
#[test]
fn test_v1_to_v2_migration() {
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use chacha20poly1305::{
ChaCha20Poly1305,
aead::{Aead, AeadCore, KeyInit, OsRng},
};
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let v1_secrets = serde_json::json!({
"OLD_KEY": "old-value",
"MULTILINE": "line1\nline2\nline3"
});
let plaintext = serde_json::to_vec(&v1_secrets).unwrap();
let cipher = ChaCha20Poly1305::new(&key.into());
let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
let ciphertext = cipher.encrypt(&nonce, plaintext.as_ref()).unwrap();
let v1_file = serde_json::json!({
"version": 1,
"nonce": BASE64.encode(&nonce),
"ciphertext": BASE64.encode(&ciphertext)
});
fs::write(&vault_path, serde_json::to_string_pretty(&v1_file).unwrap()).unwrap();
let v = vault::Vault::load(&vault_path, key).unwrap();
assert_eq!(v.get("OLD_KEY"), Some(&"old-value".to_string()));
assert_eq!(v.get("MULTILINE"), Some(&"line1\nline2\nline3".to_string()));
let entry = v.get_entry("OLD_KEY").unwrap();
assert_eq!(entry.format, SecretFormat::Raw);
assert!(entry.description.is_none());
let ml_entry = v.get_entry("MULTILINE").unwrap();
assert_eq!(ml_entry.format, SecretFormat::Multiline);
}
#[test]
fn test_migration_saves_as_v2() {
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use chacha20poly1305::{
ChaCha20Poly1305,
aead::{Aead, AeadCore, KeyInit, OsRng},
};
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let v1_secrets = serde_json::json!({"KEY": "value"});
let plaintext = serde_json::to_vec(&v1_secrets).unwrap();
let cipher = ChaCha20Poly1305::new(&key.into());
let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
let ciphertext = cipher.encrypt(&nonce, plaintext.as_ref()).unwrap();
let v1_file = serde_json::json!({
"version": 1,
"nonce": BASE64.encode(&nonce),
"ciphertext": BASE64.encode(&ciphertext)
});
fs::write(&vault_path, serde_json::to_string(&v1_file).unwrap()).unwrap();
let v = vault::Vault::load(&vault_path, key).unwrap();
v.save().unwrap();
#[derive(serde::Deserialize)]
struct EncFile {
version: u8,
}
let raw = fs::read_to_string(&vault_path).unwrap();
let parsed: EncFile = serde_json::from_str(&raw).unwrap();
assert_eq!(parsed.version, 2);
let v2 = vault::Vault::load(&vault_path, key).unwrap();
assert_eq!(v2.get("KEY"), Some(&"value".to_string()));
}
#[test]
fn test_set_hash() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set("KEY", "value");
assert!(v.get_entry("KEY").unwrap().hash.is_none());
assert!(v.set_hash("KEY", Some("abc123".to_string())));
assert_eq!(v.get_entry("KEY").unwrap().hash, Some("abc123".to_string()));
assert!(v.set_hash("KEY", None));
assert!(v.get_entry("KEY").unwrap().hash.is_none());
assert!(!v.set_hash("GHOST", Some("hash".to_string())));
}
#[test]
fn test_update_clears_hash() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let mut v = vault::Vault::load(&vault_path, key).unwrap();
v.set("KEY", "original");
v.set_hash("KEY", Some("original-hash".to_string()));
assert_eq!(
v.get_entry("KEY").unwrap().hash,
Some("original-hash".to_string())
);
v.set("KEY", "new-value");
assert!(v.get_entry("KEY").unwrap().hash.is_none());
}
#[test]
fn test_empty_vault() {
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let v = vault::Vault::load(&vault_path, key).unwrap();
assert!(v.list().is_empty());
assert!(v.list_entries().is_empty());
assert!(v.get("ANY").is_none());
assert!(v.get_entry("ANY").is_none());
}
#[test]
fn test_unsupported_vault_version() {
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use chacha20poly1305::{
ChaCha20Poly1305,
aead::{Aead, AeadCore, KeyInit, OsRng},
};
let dir = tempdir().unwrap();
let vault_path = dir.path().join("vault.enc");
let key = get_test_key();
let plaintext = b"{}";
let cipher = ChaCha20Poly1305::new(&key.into());
let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
let ciphertext = cipher.encrypt(&nonce, plaintext.as_ref()).unwrap();
let fake_vault = serde_json::json!({
"version": 99,
"nonce": BASE64.encode(&nonce),
"ciphertext": BASE64.encode(&ciphertext)
});
fs::write(&vault_path, serde_json::to_string(&fake_vault).unwrap()).unwrap();
let result = vault::Vault::load(&vault_path, key);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Unsupported vault version") || err_msg.contains("99"));
}
}