// SPDX-License-Identifier: MPL-2.0 use std::path::Path; use std::process::Command; use assert_cmd::assert::OutputAssertExt; use assert_cmd::cargo::CommandCargoExt; use assert_fs::fixture::FileWriteStr; use assert_fs::NamedTempFile; use predicates::prelude::predicate; fn get_passphrase(stdout: &[u8]) -> Option { let stdout = String::from_utf8(stdout.to_vec()).unwrap(); for line in stdout.lines() { if let Some(stripped) = line.strip_prefix("The new passphrase for this database file is:") { return Some(stripped.trim().to_owned()); } } None } fn init_db(path: &Path) -> String { let cmd = Command::cargo_bin(assert_cmd::crate_name!()) .unwrap() .arg("init") .arg(path) .assert() .success(); let output = cmd.get_output(); get_passphrase(&output.stdout).unwrap() } fn open_and_gen_password(db_path: &Path, db_pass: &str) -> String { let mut cmd = Command::cargo_bin(assert_cmd::crate_name!()).unwrap(); cmd.arg("open").arg(db_path); let mut p = rexpect::session::spawn_command(cmd, None).unwrap(); p.exp_string("Enter the passphrase: ").unwrap(); p.send_line(db_pass).unwrap(); p.exp_string("Deriving key...").unwrap(); p.exp_string("> ").unwrap(); p.send_line("gen codeberg").unwrap(); p.exp_string("New password generated for codeberg.").unwrap(); p.exp_string("> ").unwrap(); p.send_line("show codeberg").unwrap(); let needle = "password: "; let regex = format!("{needle}.*"); let (_, matched) = p.exp_regex(®ex).unwrap(); p.exp_string("> ").unwrap(); p.send_line("exit").unwrap(); p.exp_eof().unwrap(); let site_pass = matched.strip_prefix(needle).unwrap().trim().to_owned(); site_pass } fn change_passphrase(db_path: &Path, db_pass: &str) -> String { let mut cmd = Command::cargo_bin(assert_cmd::crate_name!()).unwrap(); cmd.arg("pass").arg(db_path); let mut p = rexpect::session::spawn_command(cmd, None).unwrap(); p.exp_string("Enter the passphrase: ").unwrap(); p.send_line(db_pass).unwrap(); p.exp_string("Deriving key...").unwrap(); let needle = "The new passphrase for this database file is: "; let regex = format!("{needle}.*"); let (_, matched) = p.exp_regex(®ex).unwrap(); p.exp_string("Remember this passphrase! It will not be displayed again.") .unwrap(); p.exp_string("Deriving key...").unwrap(); p.exp_eof().unwrap(); let new_db_pass = matched.strip_prefix(needle).unwrap().trim().to_owned(); new_db_pass } fn open_and_show_password(db_path: &Path, db_pass: &str) -> String { let mut cmd = Command::cargo_bin(assert_cmd::crate_name!()).unwrap(); cmd.arg("open").arg(db_path); let mut p = rexpect::session::spawn_command(cmd, None).unwrap(); p.exp_string("Enter the passphrase: ").unwrap(); p.send_line(db_pass).unwrap(); p.exp_string("Deriving key...").unwrap(); p.exp_string("> ").unwrap(); p.send_line("show codeberg").unwrap(); let needle = "password: "; let regex = format!("{needle}.*"); let (_, matched) = p.exp_regex(®ex).unwrap(); p.exp_string("> ").unwrap(); p.send_line("exit").unwrap(); p.exp_eof().unwrap(); let site_pass = matched.strip_prefix(needle).unwrap().trim().to_owned(); site_pass } fn change_passphrase_incorrectly(db_path: &Path) { let mut cmd = Command::cargo_bin(assert_cmd::crate_name!()).unwrap(); cmd.arg("pass").arg(db_path); let mut p = rexpect::session::spawn_command(cmd, None).unwrap(); p.exp_string("Enter the passphrase: ").unwrap(); p.send_line("What's up, doc?").unwrap(); p.exp_string("Deriving key...").unwrap(); p.exp_string("Error: Did you enter the wrong passphrase?").unwrap(); p.exp_eof().unwrap(); } #[test] fn changing_passphrase_of_db_preserves_passwords() { let file = NamedTempFile::new("test.napa").unwrap(); let db_pass = init_db(file.path()); let site_pass = open_and_gen_password(file.path(), &db_pass); let new_db_pass = change_passphrase(file.path(), &db_pass); let new_site_pass = open_and_show_password(file.path(), &new_db_pass); assert_eq!(site_pass, new_site_pass); } #[test] fn pass_with_wrong_passphrase_leaves_db_unchanged() { let file = NamedTempFile::new("test.napa").unwrap(); let db_pass = init_db(file.path()); open_and_gen_password(file.path(), &db_pass); let original_contents = std::fs::read(file.path()).unwrap(); change_passphrase_incorrectly(file.path()); let new_contents = std::fs::read(file.path()).unwrap(); assert_eq!(original_contents, new_contents); } #[test] fn init_existing_file_fails() { let file_contents = "Hello world!"; let file = NamedTempFile::new("test.txt").unwrap(); file.write_str(file_contents).unwrap(); Command::cargo_bin(assert_cmd::crate_name!()) .unwrap() .arg("init") .arg(file.path()) .assert() .failure() .stderr(predicate::str::contains("Error: Unable to create file")); let new_contents = std::fs::read_to_string(file.path()).unwrap(); assert_eq!(file_contents, new_contents); } #[test] fn open_nonexistent_file_fails() { let file = NamedTempFile::new("test.napa").unwrap(); Command::cargo_bin(assert_cmd::crate_name!()) .unwrap() .arg("open") .arg(file.path()) .assert() .failure() .stderr(predicate::str::contains("Error: Unable to read file")); } #[test] fn open_too_short_file_fails() { let file_contents = "Hello world!"; let file = NamedTempFile::new("test.txt").unwrap(); file.write_str(file_contents).unwrap(); Command::cargo_bin(assert_cmd::crate_name!()) .unwrap() .arg("open") .arg(file.path()) .assert() .failure() .stderr(predicate::str::contains( "Error: Header length 12 is less than required 60", )); } #[test] fn open_with_wrong_passphrase_fails() { let file = NamedTempFile::new("test.napa").unwrap(); init_db(file.path()); let mut cmd = Command::cargo_bin(assert_cmd::crate_name!()).unwrap(); cmd.arg("open").arg(file.path()); let mut p = rexpect::session::spawn_command(cmd, None).unwrap(); p.exp_string("Enter the passphrase: ").unwrap(); p.send_line("What's up, doc?").unwrap(); p.exp_string("Deriving key...").unwrap(); p.exp_string("Error: Did you enter the wrong passphrase?").unwrap(); p.exp_eof().unwrap(); } #[test] fn pass_nonexistent_file_fails() { let file = NamedTempFile::new("test.napa").unwrap(); Command::cargo_bin(assert_cmd::crate_name!()) .unwrap() .arg("pass") .arg(file.path()) .assert() .failure() .stderr(predicate::str::contains("Error: Unable to read file")); }