From 8c8bee36e2bf7728c05fdb62c551022658d9ff28 Mon Sep 17 00:00:00 2001 From: ManOfGoldForever Date: Mon, 2 Mar 2026 10:50:01 -0500 Subject: [PATCH] Added tests --- src/args.rs | 38 ++++++++++++++++++++++++--- src/storage.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/src/args.rs b/src/args.rs index d221048..f670bfd 100644 --- a/src/args.rs +++ b/src/args.rs @@ -41,9 +41,6 @@ pub struct AddPass { pub struct GetPass { /// The name of the password to get pub name: String, - /// Copy the password to the clipboard instead of printing it - #[arg(short, long)] - pub copy: bool, } #[derive(Args)] @@ -51,3 +48,38 @@ pub struct DeletePass { /// The name of the password to delete pub name: String, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_add_command() { + let cli = Cli::try_parse_from(["pm", "add", "github"]).expect("parse add"); + match cli.command { + Some(Commands::Add(add)) => assert_eq!(add.name, "github"), + _ => panic!("expected add command"), + } + } + + #[test] + fn parses_get_command() { + let cli = Cli::try_parse_from(["pm", "get", "email"]).expect("parse get"); + match cli.command { + Some(Commands::Get(get)) => assert_eq!(get.name, "email"), + _ => panic!("expected get command"), + } + } + + #[test] + fn rejects_removed_copy_flag() { + let cli = Cli::try_parse_from(["pm", "get", "email", "--copy"]); + assert!(cli.is_err()); + } + + #[test] + fn parses_list_command() { + let cli = Cli::try_parse_from(["pm", "list"]).expect("parse list"); + assert!(matches!(cli.command, Some(Commands::List))); + } +} diff --git a/src/storage.rs b/src/storage.rs index f9879d6..5d42f96 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -118,3 +118,74 @@ pub fn verify_master_password(password: &str, recorded_hash: &str) -> bool { .verify_password(password.as_bytes(), &parsed_hash) .is_ok() } + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn temp_file_path(prefix: &str) -> String { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time went backwards") + .as_nanos(); + std::env::temp_dir() + .join(format!("pm_test_{}_{}_{}", prefix, std::process::id(), nanos)) + .to_string_lossy() + .to_string() + } + + #[test] + fn key_derivation_is_deterministic_for_same_input() { + let mut p1 = String::from("master-password"); + let mut p2 = String::from("master-password"); + let salt = "abc123fixedsalt"; + + let k1 = derive_key(&mut p1, salt); + let k2 = derive_key(&mut p2, salt); + + assert_eq!(k1, k2); + } + + #[test] + fn hash_and_verify_master_password_work() { + let password = "correct-horse-battery-staple"; + let hash = hash_master_password(password); + + assert!(verify_master_password(password, &hash)); + assert!(!verify_master_password("wrong-password", &hash)); + } + + #[test] + fn save_load_and_delete_roundtrip() { + let path = temp_file_path("vault"); + let key = [7u8; 32]; + let mut data = HashMap::new(); + data.insert("email".to_string(), "pw1".to_string()); + data.insert("bank".to_string(), "pw2".to_string()); + + save_passwords(&path, &data, &key); + let loaded = load_passwords(&path, &key); + assert_eq!(loaded, data); + + delete_password(&path, "email", &key); + let after_delete = load_passwords(&path, &key); + assert!(!after_delete.contains_key("email")); + assert_eq!(after_delete.get("bank"), Some(&"pw2".to_string())); + + let _ = std::fs::remove_file(path); + } + + #[test] + fn get_or_create_salt_reuses_existing_value() { + let path = temp_file_path("salt"); + + let first = get_or_create_salt(&path); + let second = get_or_create_salt(&path); + + assert_eq!(first, second); + assert_eq!(first.len(), 32); + + let _ = std::fs::remove_file(path); + } +}