mod common;
use common::{short_dir_path, unique_dir_path, unique_thread_dir_path};
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::thread;
use std::time::Duration;
use revault_lockbox_api::{Lockbox, LockboxOpen, OwnerSigningKeyPair, SecretVec};
use revault_vault_api::{
decode_hex, encode_hex, import_private_key, import_private_key_file, import_public_key,
public_key_fingerprint, SecretString, VaultDirectory,
};
#[test]
fn version_is_available_for_installers_and_package_managers() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let version = run_output(bin, &["--version"]);
assert_success(&version);
assert_eq!(
String::from_utf8_lossy(&version.stdout).trim(),
concat!("lockbox ", env!("CARGO_PKG_VERSION"))
);
}
#[test]
fn lbx_binary_is_alias_for_revault_cli() {
let bin = env!("CARGO_BIN_EXE_lbx");
let help = run_output(bin, &["--help"]);
assert_success(&help);
let help = String::from_utf8_lossy(&help.stderr);
assert!(help.contains("Usage: lockbox [LOCKBOX] <command> [arguments]"));
assert!(help.contains("Available commands:"));
}
#[test]
fn help_is_grouped_and_commands_have_specific_help() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let help = run_output(bin, &["--help"]);
assert_success(&help);
let help = String::from_utf8_lossy(&help.stderr);
assert!(help.contains(
"Create encrypted file archives, store secrets safely, and grant access with public keys."
));
assert!(help.contains(&format!("Version: {}", env!("CARGO_PKG_VERSION"))));
assert!(help.contains("-V, --version"));
assert!(help.contains("Usage: lockbox [LOCKBOX] <command> [arguments]"));
assert!(help.contains("Available commands:"));
assert!(help.contains("Archives"));
assert!(help.contains("Files"));
assert!(help.contains("Vault"));
assert!(help.contains("access"));
assert!(!help.contains("--jobs auto|1|N"));
let add_help = run_output(bin, &["add", "--help"]);
assert_success(&add_help);
let add_help = String::from_utf8_lossy(&add_help.stdout);
assert!(add_help.contains("Usage: lockbox add"));
assert!(add_help.contains("-r, --recursive"));
assert!(add_help.contains("--to <LOCKBOX_PATH>"));
assert!(add_help.contains("--overwrite"));
assert!(add_help.contains("<SOURCE>..."));
assert!(!add_help.contains("--jobs"));
let add_verbose_help = run_output(bin, &["add", "--help", "--verbose"]);
assert_success(&add_verbose_help);
let add_verbose_help = String::from_utf8_lossy(&add_verbose_help.stdout);
assert!(add_verbose_help.contains("--jobs <auto|1|N>"));
let mirror_help = run_output(bin, &["mirror", "--help"]);
assert_success(&mirror_help);
let mirror_help = String::from_utf8_lossy(&mirror_help.stdout);
assert!(mirror_help.contains("lockbox [LOCKBOX] mirror [NAME] <COMMAND>"));
assert!(mirror_help.contains(
"lockbox [LOCKBOX] mirror <NAME> create --from <HOST_DIRECTORY> --to <LOCKBOX_DIRECTORY>"
));
assert!(add_verbose_help.contains("--key <RAW_CONTENT_KEY>"));
assert!(add_verbose_help.contains("Context:"));
assert!(add_verbose_help.contains("Pass --recursive for a directory source"));
assert_contains_in_order(
&add_verbose_help,
&[
"Add a file or directory to a lockbox.",
"Context:",
"Usage: lockbox add",
],
);
let env_help = run_output(bin, &["variable", "set", "--help"]);
assert_success(&env_help);
let env_help = String::from_utf8_lossy(&env_help.stdout);
assert!(env_help.contains("-v, --value <VALUE>"));
assert!(env_help.contains("not accepted with --secret"));
assert!(!env_help.contains("Context:"));
let env_help = run_output(bin, &["variable", "--help"]);
assert_success(&env_help);
let env_help = String::from_utf8_lossy(&env_help.stdout);
assert!(env_help.contains("Print one stored variable value by name."));
assert!(env_help.contains("Print all non-secret variable values in an importable format."));
assert!(!env_help.contains("Normal values are printed by `variable get`"));
let plural_alias = run_output(bin, &["variables", "--help"]);
assert_success(&plural_alias);
let variable_move_help = run_output(bin, &["variable", "mv", "--help"]);
assert_success(&variable_move_help);
let variable_move_help = String::from_utf8_lossy(&variable_move_help.stdout);
assert!(variable_move_help.contains("Move matching variables"));
assert!(variable_move_help
.contains("Usage: lockbox [LOCKBOX] variable move [OPTIONS] <SOURCE> <DESTINATION>"));
assert!(!variable_move_help.contains("[LOCKBOX] <SOURCE>"));
let env_verbose_help = run_output(bin, &["variable", "--help", "--verbose"]);
assert_success(&env_verbose_help);
let env_verbose_help = String::from_utf8_lossy(&env_verbose_help.stdout);
assert!(env_verbose_help.contains("Context:"));
assert!(env_verbose_help.contains("Variables let you store name/value pairs securely"));
assert!(env_verbose_help.contains("set the variable using the --secret flag"));
assert!(env_verbose_help.contains("additional level of security"));
let form_help = run_output(bin, &["form", "--help"]);
assert_success(&form_help);
let form_help = String::from_utf8_lossy(&form_help.stdout);
assert!(form_help.contains("Manage typed multi-field form records."));
assert!(form_help.contains("define"));
assert!(form_help.contains("definitions"));
assert!(form_help.contains("capture"));
assert!(form_help.contains("add"));
assert!(form_help.contains("show"));
assert!(form_help.contains("remove"));
let form_move_help = run_output(bin, &["form", "mv", "--help"]);
assert_success(&form_move_help);
assert!(String::from_utf8_lossy(&form_move_help.stdout).contains("Move matching form records"));
let form_define_help = run_output(bin, &["form", "define", "--help"]);
assert_success(&form_define_help);
let form_define_help = String::from_utf8_lossy(&form_define_help.stdout);
assert!(form_define_help.contains("--definition-id <DEFINITION_ID>"));
assert!(form_define_help.contains("--description <TEXT>"));
assert!(form_define_help.contains("--field <NAME[:KIND[:required[:LABEL]]]>"));
assert!(!form_define_help.contains("--type-id"));
let form_define_verbose_help = run_output(bin, &["form", "define", "--help", "--verbose"]);
assert_success(&form_define_verbose_help);
let form_define_verbose_help = String::from_utf8_lossy(&form_define_verbose_help.stdout);
assert!(form_define_verbose_help.contains("NAME[:KIND[:required[:LABEL]]]"));
assert!(form_define_verbose_help.contains("The alias is optional"));
assert!(form_define_verbose_help.contains("lockbox secrets.lbox form define --name Login"));
let form_define_error = run_output(bin, &["test.lbox", "form", "define"]);
assert!(!form_define_error.status.success());
let form_define_error = String::from_utf8_lossy(&form_define_error.stderr);
assert!(form_define_error.contains("Example:"));
assert!(form_define_error.contains("lockbox secrets.lbox form define login"));
assert!(form_define_error.contains("[alias]"));
let dir = unique_dir_named("form-define-separator");
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("forms.lbox");
let form_define_with_separator = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"form",
"define",
"login",
"--",
"--field",
"username:text",
"--field",
"password:secret",
],
);
assert_success(&form_define_with_separator);
assert!(String::from_utf8_lossy(&form_define_with_separator.stdout).contains("fields: 2"));
let form_set_help = run_output(bin, &["form", "set", "--help"]);
assert_success(&form_set_help);
let form_set_help = String::from_utf8_lossy(&form_set_help.stdout);
assert!(form_set_help.contains("-v, --value <VALUE>"));
assert!(form_set_help.contains("-f, --file <FILE>"));
assert!(form_set_help.contains("-e, --from-env <NAME>"));
assert!(form_set_help.contains("-i, --interactive"));
let env_set_verbose_help = run_output(bin, &["variable", "set", "--help", "--verbose"]);
assert_success(&env_set_verbose_help);
let env_set_verbose_help = String::from_utf8_lossy(&env_set_verbose_help.stdout);
assert!(env_set_verbose_help.contains("Context:"));
assert!(env_set_verbose_help.contains("Choose one value source"));
assert!(env_set_verbose_help.contains("Secret values cannot use --value"));
let env_export_help = run_output(bin, &["variable", "export", "--help"]);
assert_success(&env_export_help);
let env_export_help = String::from_utf8_lossy(&env_export_help.stdout);
assert!(env_export_help.contains("[PATTERN]"));
let env_get_help = run_output(bin, &["variable", "get", "--help"]);
assert_success(&env_get_help);
let env_get_help = String::from_utf8_lossy(&env_get_help.stdout);
assert!(env_get_help.contains("lockbox secrets.lbox variable get APP_MODE"));
assert!(env_get_help.contains("lockbox secrets.lbox variable get --secret API_TOKEN"));
assert!(env_get_help.contains("--output <FILE>"));
assert!(env_get_help
.contains("lockbox secrets.lbox variable get --secret --output api-token.txt API_TOKEN"));
let form_get_help = run_output(bin, &["form", "get", "--help"]);
assert_success(&form_get_help);
let form_get_help = String::from_utf8_lossy(&form_get_help.stdout);
assert!(form_get_help.contains("lockbox secrets.lbox form get /work/github username"));
assert!(form_get_help.contains("lockbox secrets.lbox form get --secret"));
assert!(form_get_help.contains("--output <FILE>"));
assert!(form_get_help.contains("--overwrite"));
let env_export_help = run_output(bin, &["variable", "export", "--help"]);
assert_success(&env_export_help);
let env_export_help = String::from_utf8_lossy(&env_export_help.stdout);
assert!(env_export_help.contains("--format <posix|powershell|cmd|json>"));
assert!(env_export_help.contains("eval \"$(lockbox secrets.lbox variable export)\""));
assert!(env_export_help.contains("Use shell redirection to write it to a file."));
let vault_init_help = run_output(bin, &["vault", "init", "--help"]);
assert_success(&vault_init_help);
let vault_init_help = String::from_utf8_lossy(&vault_init_help.stdout);
assert!(!vault_init_help.contains("A new vault also gets a default profile."));
assert!(vault_init_help.contains("--verify"));
assert!(vault_init_help.contains("--overwrite"));
let vault_init_verbose_help = run_output(bin, &["vault", "init", "--help", "--verbose"]);
assert_success(&vault_init_verbose_help);
let vault_init_verbose_help = String::from_utf8_lossy(&vault_init_verbose_help.stdout);
assert!(vault_init_verbose_help.contains("Context:"));
assert!(vault_init_verbose_help.contains("A new Vault also gets a default Profile."));
let vault_beget_help = run_output(bin, &["vault", "beget", "--help"]);
assert_success(&vault_beget_help);
let vault_beget_help = String::from_utf8_lossy(&vault_beget_help.stdout);
assert!(vault_beget_help.contains("Usage: lockbox vault beget [OPTIONS] <PROFILE>"));
assert!(vault_beget_help.contains("--output <VAULT>"));
assert!(vault_beget_help.contains("--contact-name <NAME>"));
assert!(vault_beget_help.contains("--no-contact"));
assert!(vault_beget_help.contains("Defaults to <PROFILE>.vault.lbx"));
let vault_passphrase_help = run_output(bin, &["vault", "passphrase", "--help"]);
assert_success(&vault_passphrase_help);
let vault_passphrase_help = String::from_utf8_lossy(&vault_passphrase_help.stdout);
assert!(vault_passphrase_help.contains("Change the Vault passphrase."));
let vault_profile_create_help = run_output(bin, &["vault", "profile", "create", "--help"]);
assert_success(&vault_profile_create_help);
let vault_profile_create_help = String::from_utf8_lossy(&vault_profile_create_help.stdout);
assert!(vault_profile_create_help.contains("Create one of your profiles."));
assert!(!vault_profile_create_help.contains("creates the `default` profile"));
assert!(vault_profile_create_help
.contains("lockbox vault profile export ./laptop.pub --name laptop"));
let removed_profile_term = run_output(bin, &["vault", "identity", "list"]);
assert!(!removed_profile_term.status.success());
assert!(
String::from_utf8_lossy(&removed_profile_term.stderr).contains("unrecognized subcommand")
);
let profile_publish_help = run_output(bin, &["vault", "profile", "publish", "--help"]);
assert_success(&profile_publish_help);
let profile_publish_help = String::from_utf8_lossy(&profile_publish_help.stdout);
assert!(profile_publish_help.contains("Publish one profile public key"));
let profile_fingerprint_help = run_output(bin, &["vault", "profile", "fingerprint", "--help"]);
assert_success(&profile_fingerprint_help);
let profile_fingerprint_help = String::from_utf8_lossy(&profile_fingerprint_help.stdout);
assert!(profile_fingerprint_help.contains("Show the publish fingerprint"));
let contact_receive_verbose_help =
run_output(bin, &["vault", "contact", "receive", "--help", "--verbose"]);
assert_success(&contact_receive_verbose_help);
let contact_receive_verbose_help =
String::from_utf8_lossy(&contact_receive_verbose_help.stdout);
assert!(contact_receive_verbose_help.contains("Short PINs"));
assert!(contact_receive_verbose_help.contains("authenticate a public key"));
assert!(contact_receive_verbose_help.contains("You must initiate the communication"));
assert!(contact_receive_verbose_help.contains("do not accept it"));
for removed in ["share", "publish", "receive", "remove"] {
let output = run_output(bin, &["vault", removed, "--help"]);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("unrecognized subcommand"));
}
let vault_form_help = run_output(bin, &["vault", "form", "--help"]);
assert_success(&vault_form_help);
let vault_form_help = String::from_utf8_lossy(&vault_form_help.stdout);
assert!(vault_form_help.contains("Manage reusable form definitions."));
assert!(vault_form_help.contains("define"));
assert!(vault_form_help.contains("list"));
assert!(!vault_profile_create_help.contains("export-public"));
assert!(vault_profile_create_help.contains("lockbox vault profile create laptop\n"));
assert!(!vault_profile_create_help.contains("[public-key-output]"));
let vault_profile_create_verbose_help =
run_output(bin, &["vault", "profile", "create", "--help", "--verbose"]);
assert_success(&vault_profile_create_verbose_help);
let vault_profile_create_verbose_help =
String::from_utf8_lossy(&vault_profile_create_verbose_help.stdout);
assert!(vault_profile_create_verbose_help.contains("Context:"));
assert!(vault_profile_create_verbose_help.contains("creates the `default` profile"));
let vault_profile_help = run_output(bin, &["vault", "profile", "--help"]);
assert_success(&vault_profile_help);
let vault_profile_help = String::from_utf8_lossy(&vault_profile_help.stdout);
assert!(vault_profile_help.contains("Manage your lockbox open profiles."));
assert!(!vault_profile_help.contains("has a public key and a private key"));
assert!(!vault_profile_help.contains("lockbox vault contact import"));
assert!(!vault_profile_help.contains("on this machine"));
assert!(vault_profile_help.contains("list"));
assert!(vault_profile_help.contains("create"));
assert!(vault_profile_help.contains("backup"));
assert!(vault_profile_help.contains("restore"));
assert!(vault_profile_help.contains("export"));
assert!(!vault_profile_help.contains("export-private"));
assert!(!vault_profile_help.contains("export-public"));
assert!(!vault_profile_help.contains(" help"));
let vault_profile_verbose_help = run_output(bin, &["vault", "profile", "--help", "--verbose"]);
assert_success(&vault_profile_verbose_help);
let vault_profile_verbose_help = String::from_utf8_lossy(&vault_profile_verbose_help.stdout);
assert!(vault_profile_verbose_help.contains("Context:"));
assert!(vault_profile_verbose_help.contains("has a public key, private open key"));
assert!(vault_profile_verbose_help.contains("Publish or export the public key"));
assert!(vault_profile_verbose_help.contains("profile backup and restore"));
assert!(!vault_profile_verbose_help.contains("on this machine"));
assert_contains_in_order(
&vault_profile_verbose_help,
&[
"Manage your lockbox open profiles.",
"Context:",
"Usage: lockbox vault profile",
],
);
let export_public_help = run_output(bin, &["vault", "profile", "export-public", "--help"]);
assert!(!export_public_help.status.success());
assert!(String::from_utf8_lossy(&export_public_help.stderr).contains("unrecognized subcommand"));
let vault_contact_help = run_output(bin, &["vault", "contact", "--help"]);
assert_success(&vault_contact_help);
let vault_contact_help = String::from_utf8_lossy(&vault_contact_help.stdout);
assert!(vault_contact_help.contains("Manage contacts that can be given access to a lockbox."));
assert!(!vault_contact_help.contains("Contacts are saved public keys"));
assert!(!vault_contact_help.contains("on this machine"));
assert!(vault_contact_help.contains("list"));
assert!(vault_contact_help.contains("import"));
assert!(vault_contact_help.contains("remove"));
let vault_contact_verbose_help = run_output(bin, &["vault", "contact", "--help", "--verbose"]);
assert_success(&vault_contact_verbose_help);
let vault_contact_verbose_help = String::from_utf8_lossy(&vault_contact_verbose_help.stdout);
assert!(vault_contact_verbose_help.contains("Context:"));
assert!(vault_contact_verbose_help.contains("Contacts are saved public keys"));
assert!(vault_contact_verbose_help.contains("opening requires the matching private profile"));
assert!(!vault_contact_verbose_help.contains("on this machine"));
let contact_list_verbose_help =
run_output(bin, &["vault", "contact", "list", "--help", "--verbose"]);
assert_success(&contact_list_verbose_help);
let contact_list_verbose_help = String::from_utf8_lossy(&contact_list_verbose_help.stdout);
assert!(contact_list_verbose_help.contains("already passed fingerprint verification"));
assert!(contact_list_verbose_help.contains("no separate trust-state"));
let access_help = run_output(bin, &["access", "--help"]);
assert_success(&access_help);
let access_help = String::from_utf8_lossy(&access_help.stdout);
assert!(access_help.contains("Grant or revoke who can open a lockbox."));
assert!(access_help.contains("grant"));
assert!(access_help.contains("revoke"));
assert!(!access_help.contains(" help"));
let access_list_verbose_help = run_output(bin, &["access", "list", "--help", "--verbose"]);
assert_success(&access_list_verbose_help);
let access_list_verbose_help = String::from_utf8_lossy(&access_list_verbose_help.stdout);
assert!(access_list_verbose_help.contains("Contact names are not stored"));
assert!(access_list_verbose_help.contains("cannot identify or correlate"));
let access_revoke_help = run_output(bin, &["access", "revoke", "--help"]);
assert_success(&access_revoke_help);
let access_revoke_help = String::from_utf8_lossy(&access_revoke_help.stdout);
assert!(access_revoke_help.contains("NAME_OR_SLOT_ID"));
let access_revoke_verbose_help = run_output(bin, &["access", "revoke", "--help", "--verbose"]);
assert_success(&access_revoke_verbose_help);
let access_revoke_verbose_help = String::from_utf8_lossy(&access_revoke_verbose_help.stdout);
assert!(access_revoke_verbose_help.contains("local profile/contact name"));
assert!(access_revoke_verbose_help.contains("pass the slot id"));
let access_grant_verbose_help = run_output(bin, &["access", "grant", "--help", "--verbose"]);
assert_success(&access_grant_verbose_help);
let access_grant_verbose_help = String::from_utf8_lossy(&access_grant_verbose_help.stdout);
assert!(access_grant_verbose_help.contains("profile:name or contact:name"));
assert!(access_grant_verbose_help.contains("lockbox secrets.lbox access grant profile:alice"));
assert!(
access_grant_verbose_help.contains("lockbox secrets.lbox access grant alice ./alice.pub")
);
assert!(access_grant_verbose_help.contains("Profile name, contact name, profile:name"));
assert!(access_grant_verbose_help.contains("Public key path"));
let vault_help = run_output(bin, &["vault", "--help"]);
assert_success(&vault_help);
let vault_help = String::from_utf8_lossy(&vault_help.stdout);
assert!(vault_help.contains("Manage profiles, contacts, and reusable forms."));
assert!(!vault_help.contains("sessions"));
assert!(!vault_help.contains("doctor"));
assert!(vault_help.contains("profile"));
assert!(vault_help.contains("contact"));
assert!(vault_help.contains("form"));
assert!(!vault_help.contains(" key"));
assert!(!vault_help.contains("trust"));
assert!(!vault_help.contains("credentials"));
assert!(!vault_help.contains("platform-store"));
assert!(!vault_help.contains(" open"));
assert!(!vault_help.contains(" help"));
assert!(!vault_help.contains("path"));
let vault_path = run_output(bin, &["vault", "path"]);
assert!(!vault_path.status.success());
assert!(String::from_utf8_lossy(&vault_path.stderr).contains("unrecognized subcommand"));
let sessions_help = run_output(bin, &["session", "--help"]);
assert_success(&sessions_help);
let sessions_help = String::from_utf8_lossy(&sessions_help.stdout);
assert!(
sessions_help.contains("Manage the default Lockbox and keys cached by the Session Agent.")
);
assert!(sessions_help.contains("default"));
assert!(sessions_help.contains("--clear"));
assert!(sessions_help.contains("close-all"));
assert!(sessions_help.contains("auto-open"));
assert!(sessions_help.contains("stop"));
let auto_open_help = run_output(bin, &["session", "auto-open", "--help"]);
assert_success(&auto_open_help);
let auto_open_help = String::from_utf8_lossy(&auto_open_help.stdout);
assert!(auto_open_help.contains("Allow reVault to use your OS login"));
assert!(auto_open_help.contains("status"));
assert!(auto_open_help.contains("disable"));
assert!(auto_open_help.contains("vault"));
assert!(auto_open_help.contains("lockboxes"));
assert!(auto_open_help.contains("--yes"));
let doctor_help = run_output(bin, &["doctor", "--help"]);
assert_success(&doctor_help);
let doctor_help = String::from_utf8_lossy(&doctor_help.stdout);
assert!(doctor_help.contains("Diagnose and maintain vaults and lockboxes."));
assert!(doctor_help.contains("lockbox secrets.lbox doctor"));
assert!(doctor_help.contains("recover"));
let open_help = run_output(bin, &["open", "--help"]);
assert_success(&open_help);
let open_help = String::from_utf8_lossy(&open_help.stdout);
assert!(open_help.contains("Open the lockbox for later commands."));
assert!(open_help.contains("--duration <DURATION>"));
assert!(open_help.contains("--password-env <NAME>"));
assert!(open_help.contains("--password-file <FILE>"));
assert!(open_help.contains("--password-stdin"));
assert!(!open_help.contains("--list"));
let open_verbose_help = run_output(bin, &["open", "--help", "--verbose"]);
assert_success(&open_verbose_help);
let open_verbose_help = String::from_utf8_lossy(&open_verbose_help.stdout);
assert!(open_verbose_help.contains("Close the Lockbox when you have finished working"));
assert!(open_verbose_help.contains("automatically close the Lockbox after 30 minutes"));
let close_help = run_output(bin, &["close", "--help"]);
assert_success(&close_help);
let close_help = String::from_utf8_lossy(&close_help.stdout);
assert!(close_help.contains("Close the lockbox."));
assert!(close_help.contains("lockbox secrets.lbox close"));
assert!(close_help.contains("lockbox close"));
assert!(!close_help.contains("--all"));
let close_verbose_help = run_output(bin, &["close", "--help", "--verbose"]);
assert_success(&close_verbose_help);
let close_verbose_help = String::from_utf8_lossy(&close_verbose_help.stdout);
assert!(close_verbose_help.contains("Closes the given Lockbox or the default Lockbox"));
assert!(close_verbose_help.contains("automatically close the Lockbox after 30 minutes"));
let recover_help = run_output(bin, &["damaged.lbox", "doctor", "recover", "--help"]);
assert_success(&recover_help);
let recover_help = String::from_utf8_lossy(&recover_help.stdout);
assert!(recover_help.contains("lockbox damaged.lbox doctor recover"));
assert!(recover_help.contains("--dry-run"));
assert!(!recover_help.contains("--transaction"));
assert!(!recover_help.contains("--report"));
let removed_top_level = run_output(bin, &["recover", "--help"]);
assert_success(&removed_top_level);
assert!(!String::from_utf8_lossy(&removed_top_level.stdout)
.contains("Recover a lockbox using the safest applicable operation."));
}
#[test]
fn target_first_lockbox_is_rejected_for_global_commands() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let output = run_output(bin, &["secrets.lbox", "vault", "init"]);
assert!(!output.status.success());
assert!(
String::from_utf8_lossy(&output.stderr).contains("vault is not a lockbox-scoped command")
);
}
#[test]
fn command_first_lockbox_is_rejected_for_lockbox_scoped_commands() {
let bin = env!("CARGO_BIN_EXE_lockbox");
for args in [
vec!["open", "test.lbox"],
vec!["list", "test.lbox"],
vec!["variable", "get", "test.lbox", "API_KEY"],
vec!["form", "show", "test.lbox", "/login"],
] {
let output = Command::new(bin).args(&args).output().unwrap();
assert!(
!output.status.success(),
"legacy command ordering unexpectedly succeeded: {args:?}"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("unexpected argument")
|| stderr.contains("lockbox paths must precede lockbox-scoped commands"),
"unexpected error for {args:?}: {stderr}"
);
}
}
#[test]
fn form_definitions_and_records_flow() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("forms-flow");
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("forms.lbox");
let lockbox = lockbox.to_string_lossy().to_string();
let define = run_output(
bin,
&[
&lockbox,
"form",
"define",
"login",
"--name",
"Login",
"--description",
"Website sign-in credentials",
"--field",
"username:text:required:Username",
"--field",
"password:secret:required:Password",
"--field",
"site:url",
],
);
assert_success(&define);
let define = String::from_utf8_lossy(&define.stdout);
assert!(define.contains("Form definition saved."));
assert!(define.contains("alias: login"));
assert!(define.contains("definition_id:"));
assert!(define.contains("revision: 1"));
assert!(define.contains("description: Website sign-in credentials"));
assert!(define.contains("fields: 3"));
let definition_id = define
.lines()
.find_map(|line| line.trim().strip_prefix("definition_id:"))
.map(str::trim)
.expect("definition id");
run(
bin,
&[
&lockbox,
"form",
"define",
"login",
"--definition-id",
definition_id,
"--name",
"Login",
"--description",
"Website sign-in credentials",
"--field",
"username:text:required:Username",
"--field",
"password:secret:required:Password",
"--field",
"site:url",
],
);
let aliasless_lockbox = dir.join("aliasless.lbox");
let aliasless_lockbox = aliasless_lockbox.to_string_lossy().to_string();
let aliasless_define = run_output(
bin,
&[
&aliasless_lockbox,
"form",
"define",
"--name",
"Token",
"--field",
"value:secret",
],
);
assert_success(&aliasless_define);
let aliasless_define = String::from_utf8_lossy(&aliasless_define.stdout);
assert!(aliasless_define.contains("alias: token"));
assert!(aliasless_define.contains("name: Token"));
let aliasless_list = run_output(
bin,
&[&aliasless_lockbox, "form", "definitions", "--format", "tsv"],
);
assert_success(&aliasless_list);
let aliasless_list = String::from_utf8_lossy(&aliasless_list.stdout);
assert!(aliasless_list.contains("token"));
assert!(aliasless_list.contains("Token"));
let definitions = run_output(bin, &[&lockbox, "form", "definitions", "--format", "tsv"]);
assert_success(&definitions);
let definitions = String::from_utf8_lossy(&definitions.stdout);
assert!(definitions.contains("Website sign-in credentials"));
run(
bin,
&[
&lockbox,
"form",
"add",
"/work/github",
"--type",
"login",
"--name",
"GitHub",
"--set",
"username=bsutton",
"--set",
"site=https://github.com",
],
);
let secret_set = run_output_with_stdin(
bin,
&[
&lockbox,
"form",
"set",
"--secret",
"--stdin",
"/work/github",
"password",
],
"correct horse\n",
);
assert_success(&secret_set);
assert_eq!(
String::from_utf8_lossy(&secret_set.stdout),
"/work/github\tpassword\tupdated\n"
);
let username = run_output(bin, &[&lockbox, "form", "get", "/work/github", "username"]);
assert_success(&username);
assert_eq!(String::from_utf8_lossy(&username.stdout), "bsutton\n");
run(
bin,
&[
&lockbox,
"form",
"set",
"--value",
"alice",
"/work/github",
"username",
],
);
let username = run_output(bin, &[&lockbox, "form", "get", "/work/github", "username"]);
assert_success(&username);
assert_eq!(String::from_utf8_lossy(&username.stdout), "alice\n");
let from_env = run_output_with_env(
bin,
&[
&lockbox,
"form",
"set",
"--from-env",
"E2E_FORM_VALUE",
"/work/github",
"username",
],
"E2E_FORM_VALUE",
"environment-user",
);
assert_success(&from_env);
let interactive_set = run_output_with_stdin(
bin,
&[
&lockbox,
"form",
"set",
"--interactive",
"/work/github",
"username",
],
"prompt-user\n",
);
assert_success(&interactive_set);
run(
bin,
&[
&lockbox,
"form",
"set",
"--value",
"alice",
"/work/github",
"username",
],
);
let site_file = dir.join("site.txt");
fs::write(&site_file, "https://example.com\n").unwrap();
run(
bin,
&[
&lockbox,
"form",
"set",
"--file",
site_file.to_str().unwrap(),
"/work/github",
"site",
],
);
let site = run_output(bin, &[&lockbox, "form", "get", "/work/github", "site"]);
assert_success(&site);
assert_eq!(
String::from_utf8_lossy(&site.stdout),
"https://example.com\n"
);
let password_file = dir.join("form-password.txt");
fs::write(&password_file, "file horse\n").unwrap();
run(
bin,
&[
&lockbox,
"form",
"set",
"--secret",
"--file",
password_file.to_str().unwrap(),
"/work/github",
"password",
],
);
let password = run_output(
bin,
&[
&lockbox,
"form",
"get",
"--secret",
"/work/github",
"password",
],
);
assert_success(&password);
assert_eq!(String::from_utf8_lossy(&password.stdout), "file horse\n");
let refused = run_output(bin, &[&lockbox, "form", "get", "/work/github", "password"]);
assert!(!refused.status.success());
let refused = String::from_utf8_lossy(&refused.stderr);
assert!(refused.contains("pass --secret"));
assert!(!refused.contains("Check the supplied value"));
let password = run_output(
bin,
&[
&lockbox,
"form",
"get",
"--secret",
"/work/github",
"password",
],
);
assert_success(&password);
assert_eq!(String::from_utf8_lossy(&password.stdout), "file horse\n");
let password_output = dir.join("password.txt");
let password_file = run_output(
bin,
&[
&lockbox,
"form",
"get",
"--secret",
"--output",
password_output.to_str().unwrap(),
"/work/github",
"password",
],
);
assert_success(&password_file);
assert!(password_file.stdout.is_empty());
assert_eq!(fs::read(&password_output).unwrap(), b"file horse");
let rejected_password_file = run_output(
bin,
&[
&lockbox,
"form",
"get",
"--secret",
"--output",
password_output.to_str().unwrap(),
"/work/github",
"password",
],
);
assert!(!rejected_password_file.status.success());
assert_eq!(fs::read(&password_output).unwrap(), b"file horse");
run(
bin,
&[
&lockbox,
"form",
"get",
"--output",
password_output.to_str().unwrap(),
"--overwrite",
"/work/github",
"username",
],
);
assert_eq!(fs::read(&password_output).unwrap(), b"alice");
let inspect = run_output(bin, &[&lockbox, "form", "show", "/work/github"]);
assert_success(&inspect);
let inspect = String::from_utf8_lossy(&inspect.stdout);
assert!(inspect.contains("definition_id\t"));
assert!(!inspect.contains("type_id\t"));
assert!(inspect.contains("field\tusername\tUsername\talice"));
assert!(inspect.contains("field\tpassword\tPassword\t<secret>"));
assert!(!inspect.contains("file horse"));
let list = run_output(bin, &[&lockbox, "form", "list", "/work"]);
assert_success(&list);
let list = String::from_utf8_lossy(&list.stdout);
assert!(list.lines().next().unwrap_or("").contains("definition_id"));
assert!(!list.lines().next().unwrap_or("").contains("type_id"));
assert!(list.contains("/work/github"));
assert!(list.contains("GitHub"));
let definitions = run_output(bin, &[&lockbox, "form", "definitions"]);
assert_success(&definitions);
let definitions = String::from_utf8_lossy(&definitions.stdout);
assert!(definitions
.lines()
.next()
.unwrap_or("")
.contains("definition_id"));
assert!(!definitions.lines().next().unwrap_or("").contains("type_id"));
assert!(definitions.contains("login"));
assert!(definitions.contains("Login"));
let legacy_types = run_output(bin, &[&lockbox, "form", "types"]);
assert!(!legacy_types.status.success());
let interactive = run_output_with_stdin(
bin,
&[
&lockbox,
"form",
"add",
"/work/gitlab",
"--type",
"login",
"--interactive",
],
"alice\ninteractive secret\n\n",
);
assert_success(&interactive);
let interactive_show = run_output(bin, &[&lockbox, "form", "show", "/work/gitlab"]);
assert_success(&interactive_show);
let interactive_show = String::from_utf8_lossy(&interactive_show.stdout);
assert!(interactive_show.contains("name\tgitlab"));
assert!(interactive_show.contains("field\tusername\tUsername\talice"));
assert!(interactive_show.contains("field\tpassword\tPassword\t<secret>"));
assert!(!interactive_show.contains("interactive secret"));
run(
bin,
&[
&lockbox,
"form",
"move",
"/work/gitlab",
"/work/moved-gitlab",
],
);
let moved = run_output(
bin,
&[&lockbox, "form", "show", "/work/moved-gitlab/gitlab"],
);
assert_success(&moved);
run(
bin,
&[
&lockbox,
"form",
"add",
"/work/remove-me",
"--type",
"login",
],
);
run(bin, &[&lockbox, "form", "remove", "/work/remove-me"]);
let removed = run_output(bin, &[&lockbox, "form", "show", "/work/remove-me"]);
assert!(!removed.status.success());
run(
bin,
&[
&lockbox,
"form",
"add",
"/work/remove-alias",
"--type",
"login",
],
);
run(bin, &[&lockbox, "form", "remove", "/work/remove-alias"]);
let removed = run_output(bin, &[&lockbox, "form", "show", "/work/remove-alias"]);
assert!(!removed.status.success());
run(
bin,
&[&lockbox, "variable", "set", "/prod/API_KEY", "normal-key"],
);
let visualize = run_output(bin, &[&lockbox, "visualize"]);
assert_success(&visualize);
let visualize = String::from_utf8_lossy(&visualize.stdout);
assert!(visualize.contains("variables: 1"));
assert!(visualize.contains("form definitions: 1"));
assert!(visualize.contains("form records: 2"));
assert!(visualize.contains("variables recovered: true"));
assert!(visualize.contains("forms recovered: true"));
let report = run_output(bin, &[&lockbox, "doctor", "recover", "--dry-run"]);
assert_success(&report);
let report = String::from_utf8_lossy(&report.stdout);
assert!(report.contains("variables_recovered"));
assert!(report.contains("forms_recovered"));
assert!(report.contains("form_record_count"));
}
#[test]
fn form_set_secret_upgrades_a_normal_field() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("form-secret-upgrade");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("forms.lbox");
let lockbox = lockbox.to_string_lossy().to_string();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
run_in(
bin,
&[
&lockbox,
"form",
"define",
"account",
"--field",
"token:text:required:Token",
],
&vault_root,
&agent_root,
);
run_in(
bin,
&[
&lockbox,
"form",
"add",
"/first",
"--type",
"account",
"--set",
"token=first-normal",
],
&vault_root,
&agent_root,
);
run_in(
bin,
&[
&lockbox,
"form",
"add",
"/second",
"--type",
"account",
"--set",
"token=second-normal",
],
&vault_root,
&agent_root,
);
let upgraded = run_output_in_with_stdin(
bin,
&[
&lockbox, "form", "set", "--secret", "--stdin", "/first", "token",
],
&vault_root,
&agent_root,
"first-secret",
);
assert_success(&upgraded);
let first = run_output_in(
bin,
&[&lockbox, "form", "get", "--secret", "/first", "token"],
&vault_root,
&agent_root,
);
assert_success(&first);
assert_eq!(String::from_utf8_lossy(&first.stdout), "first-secret\n");
let second = run_output_in(
bin,
&[&lockbox, "form", "get", "--secret", "/second", "token"],
&vault_root,
&agent_root,
);
assert_success(&second);
assert_eq!(String::from_utf8_lossy(&second.stdout), "second-normal\n");
let hidden = run_output_in(
bin,
&[&lockbox, "form", "show", "/second"],
&vault_root,
&agent_root,
);
assert_success(&hidden);
let hidden = String::from_utf8_lossy(&hidden.stdout);
assert!(hidden.contains("field\ttoken\tToken\t<secret>"));
assert!(!hidden.contains("second-normal"));
let downgrade = run_output_in(
bin,
&[
&lockbox, "form", "set", "--value", "normal", "/second", "token",
],
&vault_root,
&agent_root,
);
assert!(!downgrade.status.success());
assert!(String::from_utf8_lossy(&downgrade.stderr)
.contains("form field value sensitivity does not match"));
}
#[test]
fn form_interactive_edit_handles_definition_history_and_mismatched_record() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("forms-history-interactive");
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("forms.lbox");
let lockbox = lockbox.to_string_lossy().to_string();
run(
bin,
&[
&lockbox,
"form",
"define",
"login",
"--name",
"Login",
"--field",
"username:text:required:Username",
"--field",
"legacy:text:required:Legacy",
],
);
run(
bin,
&[
&lockbox,
"form",
"add",
"/work/history",
"--type",
"login",
"--name",
"History",
"--set",
"username=alice",
"--set",
"legacy=old",
],
);
run(
bin,
&[
&lockbox,
"form",
"define",
"login",
"--name",
"Login",
"--field",
"username:text:required:Username",
"--field",
"password:secret:required:Password",
"--field",
"site:url",
],
);
let edit = run_output_with_stdin(
bin,
&[&lockbox, "form", "edit", "/work/history", "--interactive"],
"\nrevision secret\n\n",
);
assert_success(&edit);
assert!(String::from_utf8_lossy(&edit.stdout).contains("Username [alice]:"));
let show = run_output(bin, &[&lockbox, "form", "show", "/work/history"]);
assert_success(&show);
let show = String::from_utf8_lossy(&show.stdout);
assert!(show.contains("field\tusername\tUsername\talice"));
assert!(show.contains("field\tpassword\tPassword\t<secret>"));
assert!(show.contains("unknown-field\tlegacy\tLegacy"));
assert!(!show.contains("revision secret"));
let password = run_output(
bin,
&[
&lockbox,
"form",
"get",
"--secret",
"/work/history",
"password",
],
);
assert_success(&password);
assert_eq!(
String::from_utf8_lossy(&password.stdout),
"revision secret\n"
);
let edit_existing = run_output_with_stdin(
bin,
&[&lockbox, "form", "edit", "/work/history", "--interactive"],
"bob\n\nhttps://example.com\n",
);
assert_success(&edit_existing);
let edit_existing = String::from_utf8_lossy(&edit_existing.stdout);
assert!(edit_existing.contains("Username [alice]:"));
assert!(!edit_existing.contains("revision secret"));
let show = run_output(bin, &[&lockbox, "form", "show", "/work/history"]);
assert_success(&show);
let show = String::from_utf8_lossy(&show.stdout);
assert!(show.contains("field\tusername\tUsername\tbob"));
assert!(show.contains("field\tsite\tsite\thttps://example.com"));
let password = run_output(
bin,
&[
&lockbox,
"form",
"get",
"--secret",
"/work/history",
"password",
],
);
assert_success(&password);
assert_eq!(
String::from_utf8_lossy(&password.stdout),
"revision secret\n"
);
}
#[test]
fn clap_errors_are_not_double_prefixed() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let output = run_output(bin, &["vault", "create"]);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("error: unrecognized subcommand 'create'"));
assert!(!stderr.contains("error: error:"));
}
#[test]
fn unambiguous_misspelled_top_level_commands_are_not_treated_as_lockboxes() {
let bin = env!("CARGO_BIN_EXE_lbx");
for (args, misspelling, wrongly_blamed) in [
(
vec!["migrage", "vault", "--replace"],
"migrage",
"--replace",
),
(vec!["docter", "vault"], "docter", "vault"),
(vec!["opne", "secrets.lbox"], "opne", "secrets.lbox"),
(
vec!["--verbose", "migrage", "vault", "--replace"],
"migrage",
"--replace",
),
] {
let output = run_output(bin, &args);
assert!(!output.status.success(), "unexpected success for {args:?}");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains(&format!("error: unrecognized subcommand '{misspelling}'")),
"wrong error for {args:?}: {stderr}"
);
assert!(
!stderr.contains(&format!("unexpected argument '{wrongly_blamed}'")),
"later token blamed for {args:?}: {stderr}"
);
}
}
#[test]
fn open_list_flag_is_not_supported() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let output = run_output(bin, &["open", "--list"]);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("unexpected argument '--list'"));
}
#[test]
fn top_level_help_pins_command_groups_and_hidden_commands() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let help = run_output(bin, &["--help"]);
assert_success(&help);
let help = String::from_utf8_lossy(&help.stderr);
assert_contains_in_order(
&help,
&[
"Archives",
" create",
" open",
"Files",
" add",
" extract",
"Data",
" variable",
"Sharing",
" access",
"Diagnostics",
" doctor",
"Vault",
" vault",
],
);
assert!(!help.contains("keygen Generate raw keypair files."));
assert!(!help.contains("add-contact Grant access to a lockbox with another public key."));
assert!(!help.contains("list-keys List keys that can open a lockbox."));
assert!(!help.contains("remove-key Remove a key from a lockbox."));
assert!(!help.contains("LOCKBOX_KEY=<raw-content-key>"));
assert!(!help.contains("migrate Migrate"));
let verbose_help = run_output(bin, &["--help", "--verbose"]);
assert_success(&verbose_help);
let verbose_help = String::from_utf8_lossy(&verbose_help.stderr);
assert!(verbose_help.contains("Advanced global options:"));
assert!(
!verbose_help.contains("add-contact Grant access to a lockbox with another public key.")
);
assert!(!verbose_help.contains("list-keys List keys that can open a lockbox."));
assert!(!verbose_help.contains("remove-key Remove a key from a lockbox."));
assert!(verbose_help.contains("keygen Generate raw keypair files."));
assert!(verbose_help.contains("LOCKBOX_KEY=<raw-content-key>"));
assert!(verbose_help.contains("LOCKBOX_SESSION_AGENT_DIR=<dir>"));
assert!(verbose_help.contains("doctor migrate"));
let doctor_help = run_output(bin, &["doctor", "--help"]);
assert_success(&doctor_help);
assert!(String::from_utf8_lossy(&doctor_help.stdout).contains("migrate"));
let removed_top_level_migrate = run_output(bin, &["migrate", "vault", "--help"]);
assert!(!removed_top_level_migrate.status.success());
assert!(String::from_utf8_lossy(&removed_top_level_migrate.stderr)
.contains("unrecognized subcommand 'migrate'"));
let migrate_help = run_output(bin, &["doctor", "migrate", "vault", "--help"]);
assert_success(&migrate_help);
let migrate_help = String::from_utf8_lossy(&migrate_help.stderr);
assert!(!migrate_help.contains("export"));
assert!(!migrate_help.contains("upgrade"));
assert!(!migrate_help.contains("import"));
assert!(!migrate_help.contains("verify"));
let migrate_verbose_help =
run_output(bin, &["doctor", "migrate", "vault", "--help", "--verbose"]);
assert_success(&migrate_verbose_help);
let migrate_verbose_help = format!(
"{}{}",
String::from_utf8_lossy(&migrate_verbose_help.stdout),
String::from_utf8_lossy(&migrate_verbose_help.stderr)
);
assert!(migrate_verbose_help.contains("export"));
assert!(migrate_verbose_help.contains("upgrade"));
assert!(migrate_verbose_help.contains("import"));
assert!(migrate_verbose_help.contains("verify"));
let lockbox_migrate_help = run_output(bin, &["doctor", "migrate", "lockbox", "--help"]);
assert_success(&lockbox_migrate_help);
let lockbox_migrate_help = format!(
"{}{}",
String::from_utf8_lossy(&lockbox_migrate_help.stdout),
String::from_utf8_lossy(&lockbox_migrate_help.stderr)
);
assert!(!lockbox_migrate_help.contains("upgrade"));
let lockbox_migrate_verbose_help = run_output(
bin,
&["doctor", "migrate", "lockbox", "--help", "--verbose"],
);
assert_success(&lockbox_migrate_verbose_help);
let lockbox_migrate_verbose_help = format!(
"{}{}",
String::from_utf8_lossy(&lockbox_migrate_verbose_help.stdout),
String::from_utf8_lossy(&lockbox_migrate_verbose_help.stderr)
);
assert!(lockbox_migrate_verbose_help.contains("upgrade"));
let removed_archive = run_output(bin, &["doctor", "migrate", "archive", "--help"]);
assert!(!removed_archive.status.success());
assert!(String::from_utf8_lossy(&removed_archive.stderr)
.contains("unrecognized subcommand 'archive'"));
for removed in ["add-contact", "list-keys", "remove-key", "contact"] {
let output = run_output(bin, &["removed-command-test.lbox", removed, "--help"]);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains(&format!("unexpected argument '{removed}'"))
|| stderr.contains(&format!("unrecognized subcommand '{removed}'"))
);
}
for removed in ["key", "trust", "list", "ls", "doctor"] {
let output = run_output(bin, &["vault", removed, "--help"]);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("unrecognized subcommand"));
}
}
#[test]
fn direct_migration_requires_an_output_or_explicit_replacement() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let lockbox = run_output(bin, &["doctor", "migrate", "lockbox", "missing.lbox"]);
assert!(!lockbox.status.success());
assert!(String::from_utf8_lossy(&lockbox.stderr)
.contains("lockbox migration requires --output <path>, or pass --replace"));
let vault = run_output(bin, &["doctor", "migrate", "vault"]);
assert!(!vault.status.success());
assert!(String::from_utf8_lossy(&vault.stderr)
.contains("vault migration requires --output <path>, or pass --replace"));
let lockbox_without_vault = run_output(
bin,
&[
"doctor",
"migrate",
"lockbox",
"missing.lbox",
"--output",
"migrated.lbox",
],
);
assert!(!lockbox_without_vault.status.success());
assert!(String::from_utf8_lossy(&lockbox_without_vault.stderr)
.contains("lockbox migration requires a current vault"));
}
#[test]
fn file_env_and_developer_aliases_execute_real_flows() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("command-aliases");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("aliases.lbox");
let source = dir.join("source.txt");
fs::write(&source, "alpha").unwrap();
run(
bin,
&[
lockbox.to_str().unwrap(),
"add",
source.to_str().unwrap(),
"--to",
"/docs/a.txt",
],
);
run(
bin,
&[
lockbox.to_str().unwrap(),
"mv",
"/docs/a.txt",
"/docs/b.txt",
],
);
let cat = run_output(bin, &[lockbox.to_str().unwrap(), "cat", "/docs/b.txt"]);
assert_success(&cat);
assert_eq!(String::from_utf8_lossy(&cat.stdout), "alpha");
run(
bin,
&[lockbox.to_str().unwrap(), "rm", "--force", "/docs/b.txt"],
);
let listing = run_output(bin, &[lockbox.to_str().unwrap(), "ls"]);
assert_success(&listing);
let listing = String::from_utf8_lossy(&listing.stdout);
assert!(listing.contains("directory"));
assert!(listing.contains("docs/"));
run(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"APP_MODE",
"prod",
],
);
let env_list = run_output(bin, &[lockbox.to_str().unwrap(), "variable", "ls"]);
assert_success(&env_list);
assert!(String::from_utf8_lossy(&env_list.stdout).contains("APP_MODE"));
run(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"/X/A/A",
"nested",
],
);
run(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"move",
"/X/A/A",
"/X/",
],
);
let moved_variable = run_output(bin, &[lockbox.to_str().unwrap(), "variable", "get", "/X/A"]);
assert_success(&moved_variable);
assert_eq!(String::from_utf8_lossy(&moved_variable.stdout), "nested\n");
run(
bin,
&[lockbox.to_str().unwrap(), "variable", "remove", "APP_MODE"],
);
let env_list = run_output(bin, &[lockbox.to_str().unwrap(), "variable", "list"]);
assert_success(&env_list);
assert!(!String::from_utf8_lossy(&env_list.stdout).contains("APP_MODE"));
run(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"APP_MODE_ALIAS",
"prod",
],
);
run(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"remove",
"APP_MODE_ALIAS",
],
);
let env_list = run_output(bin, &[lockbox.to_str().unwrap(), "variable", "list"]);
assert_success(&env_list);
assert!(!String::from_utf8_lossy(&env_list.stdout).contains("APP_MODE_ALIAS"));
let visualize = run_output(bin, &[lockbox.to_str().unwrap(), "visualise"]);
assert_success(&visualize);
assert!(String::from_utf8_lossy(&visualize.stdout).contains("Lockbox"));
}
#[test]
fn vault_commands_execute_real_flows() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-aliases");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let public_key = dir.join("alias.pub");
let exported_public_key = dir.join("alias-export.pub");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
run_without_content_key(
bin,
&["vault", "profile", "create", "alias"],
&vault_root,
&agent_root,
);
run_without_content_key(
bin,
&["vault", "profile", "create", "alias", "--overwrite"],
&vault_root,
&agent_root,
);
let alias_lockbox = dir.join("alias-owned.lbox");
run_without_content_key(
bin,
&[alias_lockbox.to_str().unwrap(), "create", "--for", "alias"],
&vault_root,
&agent_root,
);
run_without_content_key(
bin,
&[
"vault",
"profile",
"export",
public_key.to_str().unwrap(),
"--name",
"alias",
],
&vault_root,
&agent_root,
);
run_without_content_key(
bin,
&[
"vault",
"profile",
"export",
exported_public_key.to_str().unwrap(),
"--name",
"alias",
],
&vault_root,
&agent_root,
);
assert!(
String::from_utf8_lossy(&fs::read(&exported_public_key).unwrap())
.contains("BEGIN LOCKBOX PUBLIC KEY")
);
let contact_key = import_public_key(&fs::read(&public_key).unwrap()).unwrap();
let contact_fingerprint = encode_hex(&public_key_fingerprint(&contact_key));
let bad_import = run_output_without_content_key(
bin,
&[
"vault",
"contact",
"import",
"friend",
public_key.to_str().unwrap(),
"--fingerprint",
"00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00",
"--fingerprint-channel",
"phone-call-to-owner",
],
&vault_root,
&agent_root,
);
assert!(!bad_import.status.success());
assert!(String::from_utf8_lossy(&bad_import.stderr).contains("fingerprint mismatch"));
run_without_content_key(
bin,
&[
"vault",
"contact",
"import",
"friend",
public_key.to_str().unwrap(),
"--fingerprint",
&contact_fingerprint,
"--fingerprint-channel",
"phone-call-to-owner",
],
&vault_root,
&agent_root,
);
run_without_content_key(
bin,
&[
"vault",
"contact",
"import",
"friend",
public_key.to_str().unwrap(),
"--fingerprint",
&contact_fingerprint,
"--fingerprint-channel",
"phone-call-to-owner",
"--overwrite",
],
&vault_root,
&agent_root,
);
let list = run_output_without_content_key(
bin,
&["vault", "contact", "list", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&list);
assert_eq!(String::from_utf8_lossy(&list.stdout).trim(), "friend");
run_without_content_key(
bin,
&["vault", "contact", "remove", "friend"],
&vault_root,
&agent_root,
);
run_without_content_key(
bin,
&["vault", "profile", "remove", "--force", "alias"],
&vault_root,
&agent_root,
);
let list = run_output_without_content_key(
bin,
&["vault", "profile", "list", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&list);
let list = String::from_utf8_lossy(&list.stdout);
assert!(!list.contains("alias"));
let contacts = run_output_without_content_key(
bin,
&["vault", "contact", "list", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&contacts);
assert!(!String::from_utf8_lossy(&contacts.stdout).contains("friend"));
}
#[test]
fn vault_form_definitions_can_be_used_and_captured() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-forms");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let main_lockbox = dir.join("forms-main.lbox");
let shared_lockbox = dir.join("forms-published.lbox");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
let login_definition = run_output_without_content_key(
bin,
&[
"vault",
"form",
"define",
"login",
"--field",
"username:text",
"--field",
"password:secret",
],
&vault_root,
&agent_root,
);
assert_success(&login_definition);
let login_definition = String::from_utf8_lossy(&login_definition.stdout);
let definition_id = login_definition
.lines()
.find_map(|line| line.trim().strip_prefix("definition_id:"))
.map(str::trim)
.expect("vault form definition id");
run_without_content_key(
bin,
&[
"vault",
"form",
"define",
"login",
"--definition-id",
definition_id,
"--field",
"username:text",
"--field",
"password:secret",
],
&vault_root,
&agent_root,
);
run_without_content_key(
bin,
&[
"vault",
"form",
"define",
"--name",
"Server Login",
"--description",
"Credentials for managed servers",
"--field",
"host:text",
"--field",
"password:secret",
],
&vault_root,
&agent_root,
);
let vault_definitions = run_output_without_content_key(
bin,
&["vault", "form", "list", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&vault_definitions);
let vault_definitions = String::from_utf8_lossy(&vault_definitions.stdout);
assert!(vault_definitions.contains("login"));
assert!(vault_definitions.contains("server-login"));
assert!(vault_definitions.contains("Server Login"));
assert!(vault_definitions.contains("Credentials for managed servers"));
let legacy_types =
run_output_without_content_key(bin, &["vault", "form", "types"], &vault_root, &agent_root);
assert!(!legacy_types.status.success());
run_without_content_key(
bin,
&[main_lockbox.to_str().unwrap(), "create", "--password"],
&vault_root,
&agent_root,
);
run_without_content_key(
bin,
&[main_lockbox.to_str().unwrap(), "form", "use", "login"],
&vault_root,
&agent_root,
);
let lockbox_definitions = run_output_without_content_key(
bin,
&[
main_lockbox.to_str().unwrap(),
"form",
"definitions",
"--format",
"tsv",
],
&vault_root,
&agent_root,
);
assert_success(&lockbox_definitions);
assert!(String::from_utf8_lossy(&lockbox_definitions.stdout).contains("login"));
run_without_content_key(
bin,
&[
main_lockbox.to_str().unwrap(),
"form",
"add",
"/work/github",
"--type",
"login",
"--set",
"username=bsutton",
],
&vault_root,
&agent_root,
);
let records = run_output_without_content_key(
bin,
&[
main_lockbox.to_str().unwrap(),
"form",
"list",
"--format",
"tsv",
],
&vault_root,
&agent_root,
);
assert_success(&records);
assert!(String::from_utf8_lossy(&records.stdout).contains("/work/github"));
run_without_content_key(
bin,
&[shared_lockbox.to_str().unwrap(), "create", "--password"],
&vault_root,
&agent_root,
);
run_without_content_key(
bin,
&[
shared_lockbox.to_str().unwrap(),
"form",
"define",
"server",
"--field",
"host:text",
],
&vault_root,
&agent_root,
);
run_without_content_key(
bin,
&[
shared_lockbox.to_str().unwrap(),
"form",
"capture",
"server",
"captured-server",
],
&vault_root,
&agent_root,
);
let vault_definitions = run_output_without_content_key(
bin,
&["vault", "form", "list", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&vault_definitions);
assert!(String::from_utf8_lossy(&vault_definitions.stdout).contains("captured-server"));
}
#[test]
fn negative_cli_errors_remain_specific() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("negative-cli-errors");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("negative.lbox");
let source = dir.join("source.txt");
fs::write(&source, "alpha").unwrap();
let invalid_jobs = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"add",
"--jobs",
"0",
source.to_str().unwrap(),
],
);
assert!(!invalid_jobs.status.success());
assert!(String::from_utf8_lossy(&invalid_jobs.stderr)
.contains("--jobs must be auto, 1, or a positive integer"));
let invalid_env_set = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"APP_MODE",
"prod",
"--value",
"other",
],
);
assert!(!invalid_env_set.status.success());
assert!(String::from_utf8_lossy(&invalid_env_set.stderr)
.contains("variable set requires exactly one value source"));
let invalid_secret_flag = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"var",
"set",
"-secret",
"/product/API_KEY",
"yyyyy",
],
);
assert!(!invalid_secret_flag.status.success());
let invalid_secret_flag = String::from_utf8_lossy(&invalid_secret_flag.stderr);
assert!(invalid_secret_flag.contains("unknown option: -secret"));
assert!(invalid_secret_flag.contains("Use --secret"));
assert!(!invalid_secret_flag.contains("variable set requires exactly one value source"));
let invalid_export = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"export",
"--format",
"fish",
],
);
assert!(!invalid_export.status.success());
assert!(String::from_utf8_lossy(&invalid_export.stderr)
.contains("unsupported variable export format: fish"));
}
#[test]
fn remove_requires_confirmation_and_reports_count() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let harness_dir = unique_dir();
let _ = fs::remove_dir_all(&harness_dir);
let dir = unique_dir_named("remove-confirmation");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("remove.lbox");
let source = dir.join("remove.txt");
fs::write(&source, "delete me").unwrap();
let root_source = dir.join("perf.data");
fs::write(&root_source, "root delete").unwrap();
run(
bin,
&[
lockbox.to_str().unwrap(),
"add",
source.to_str().unwrap(),
"--to",
"/docs/remove.txt",
],
);
let refused = run_output_with_stdin(
bin,
&[lockbox.to_str().unwrap(), "remove", "/docs/remove.txt"],
"no\n",
);
assert_success(&refused);
assert!(String::from_utf8_lossy(&refused.stderr)
.contains("Remove lockbox entry '/docs/remove.txt'? Type y or yes to confirm:"));
assert!(String::from_utf8_lossy(&refused.stdout).contains("No entries removed."));
let listing = run_output(bin, &[lockbox.to_str().unwrap(), "list", "--recursive"]);
assert_success(&listing);
assert!(String::from_utf8_lossy(&listing.stdout).contains("/docs/remove.txt"));
let removed = run_output_with_stdin(
bin,
&[lockbox.to_str().unwrap(), "rm", "/docs/remove.txt"],
"y\n",
);
assert_success(&removed);
assert!(String::from_utf8_lossy(&removed.stdout).contains("Removed 1 file"));
assert!(String::from_utf8_lossy(&removed.stdout).contains("/docs/remove.txt"));
run(
bin,
&[
lockbox.to_str().unwrap(),
"add",
root_source.to_str().unwrap(),
],
);
let root_removed =
run_output_with_stdin(bin, &[lockbox.to_str().unwrap(), "rm", "perf.data"], "y\n");
assert_success(&root_removed);
assert!(String::from_utf8_lossy(&root_removed.stdout).contains("/perf.data"));
let package = dir.join("package.json");
let package_lock = dir.join("package-lock.json");
fs::write(&package, "{}").unwrap();
fs::write(&package_lock, "{}").unwrap();
run(
bin,
&[
lockbox.to_str().unwrap(),
"add",
package.to_str().unwrap(),
package_lock.to_str().unwrap(),
],
);
run(
bin,
&[
lockbox.to_str().unwrap(),
"add",
package.to_str().unwrap(),
"--to",
"second/package.json",
],
);
let expanded_batch = run_output_with_stdin(
bin,
&[
lockbox.to_str().unwrap(),
"rm",
"package.json",
"package-lock.json",
],
"y\n",
);
assert_success(&expanded_batch);
assert!(String::from_utf8_lossy(&expanded_batch.stderr).contains("Remove 2 lockbox entries?"));
assert!(String::from_utf8_lossy(&expanded_batch.stdout).contains("Removed 2 entries."));
run(
bin,
&[
lockbox.to_str().unwrap(),
"add",
package.to_str().unwrap(),
package_lock.to_str().unwrap(),
],
);
let root_glob = run_output_with_stdin(bin, &[lockbox.to_str().unwrap(), "rm", "*.json"], "y\n");
assert_success(&root_glob);
assert!(String::from_utf8_lossy(&root_glob.stdout).contains("Removed 2 entries."));
let nested_still_exists = run_output(
bin,
&[lockbox.to_str().unwrap(), "cat", "second/package.json"],
);
assert_success(&nested_still_exists);
let recursive_glob =
run_output_with_stdin(bin, &[lockbox.to_str().unwrap(), "rm", "**/*.json"], "y\n");
assert_success(&recursive_glob);
assert!(String::from_utf8_lossy(&recursive_glob.stdout).contains("Removed 1 file"));
let listing = run_output(bin, &[lockbox.to_str().unwrap(), "list"]);
assert_success(&listing);
let listing = String::from_utf8_lossy(&listing.stdout);
assert!(listing.contains("directory"));
assert!(listing.contains("docs/"));
}
#[test]
fn directory_remove_requires_recursive_flag() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("remove-directory-recursive");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("remove.lbox");
let tree = dir.join("tree");
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
fs::create_dir_all(&tree).unwrap();
fs::write(tree.join("item.txt"), "nested").unwrap();
let added = run_output_in(
bin,
&[
lockbox.to_str().unwrap(),
"add",
"--recursive",
tree.to_str().unwrap(),
"--to",
"tree",
],
&vault_root,
&agent_root,
);
assert_success(&added);
let refused_directory = run_output_in(
bin,
&[lockbox.to_str().unwrap(), "remove", "--force", "tree"],
&vault_root,
&agent_root,
);
assert!(!refused_directory.status.success());
assert!(String::from_utf8_lossy(&refused_directory.stderr)
.contains("removing a directory requires --recursive"));
let removed_directory = run_output_in(
bin,
&[lockbox.to_str().unwrap(), "rm", "--force", "-r", "tree"],
&vault_root,
&agent_root,
);
assert_success(&removed_directory);
assert!(
String::from_utf8_lossy(&removed_directory.stdout).contains("Removed 1 directory: /tree")
);
}
#[test]
fn missing_lockbox_errors_are_cli_specific() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("missing-lockbox");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let missing = dir.join("missing.lbox");
let source = dir.join("source.txt");
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
fs::write(&source, "missing lockbox source").unwrap();
let output = run_output(bin, &[missing.to_str().unwrap(), "visualize"]);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("lockbox not found:"));
assert!(!stderr.contains("os error 2"));
assert!(!stderr.contains("another process is using the file"));
let open_missing = run_output_in(
bin,
&[missing.to_str().unwrap(), "open"],
&vault_root,
&agent_root,
);
assert!(!open_missing.status.success());
let stderr = String::from_utf8_lossy(&open_missing.stderr);
assert!(stderr.contains(&format!("lockbox not found: {}", missing.display())));
assert!(!stderr.contains("Could not access a file or platform service"));
assert!(!stderr.contains("os error 2"));
assert!(!stderr.contains("another process is using the file"));
let add_missing = run_output_without_content_key(
bin,
&[missing.to_str().unwrap(), "add", source.to_str().unwrap()],
&vault_root,
&agent_root,
);
assert!(!add_missing.status.success());
let stderr = String::from_utf8_lossy(&add_missing.stderr);
assert!(stderr.contains(&format!("lockbox not found: {}", missing.display())));
assert!(!stderr.contains("no creation open method"));
}
#[test]
fn removing_last_lockbox_key_has_cli_guidance() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("last-key-message");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("last-key.lbox");
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let source = dir.join("source.txt");
let private_key = dir.join("access.key");
let public_key = dir.join("access.pub");
fs::write(&source, "alpha").unwrap();
run_in(bin, &["vault", "init"], &vault_root, &agent_root);
run_in(
bin,
&[
"keygen",
private_key.to_str().unwrap(),
public_key.to_str().unwrap(),
],
&vault_root,
&agent_root,
);
run_in(
bin,
&[
lockbox.to_str().unwrap(),
"add",
source.to_str().unwrap(),
"--to",
"/alpha.txt",
],
&vault_root,
&agent_root,
);
run_in(
bin,
&[
lockbox.to_str().unwrap(),
"access",
"grant",
"access_key",
public_key.to_str().unwrap(),
],
&vault_root,
&agent_root,
);
let keys = run_output_in(
bin,
&[
lockbox.to_str().unwrap(),
"access",
"list",
"--format",
"tsv",
],
&vault_root,
&agent_root,
);
assert_success(&keys);
let slot_id = String::from_utf8_lossy(&keys.stdout)
.lines()
.find(|line| !line.trim().is_empty())
.and_then(|line| line.split('\t').next())
.expect("key slot id")
.to_string();
let output = run_output_in(
bin,
&[lockbox.to_str().unwrap(), "access", "revoke", &slot_id],
&vault_root,
&agent_root,
);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("cannot revoke the last access entry"));
assert!(stderr.contains("grant another profile or contact"));
assert!(!stderr.contains("security limit exceeded"));
assert!(!stderr.contains("Reduce the input size"));
}
#[test]
fn doctor_and_session_report_agent_state() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("agent-reporting");
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let doctor = run_output_in(bin, &["doctor"], &vault_root, &agent_root);
assert_success(&doctor);
let doctor = String::from_utf8_lossy(&doctor.stdout);
assert!(doctor.contains("Session Agent"));
assert!(doctor.contains("suspend management:"));
assert!(doctor.contains("suspend notifications:"));
assert!(doctor.contains("sleep prevention:"));
assert!(doctor.contains("running: no"));
assert!(doctor.contains("open lockboxes:"));
assert!(doctor.contains("log:"));
assert!(doctor.contains(agent_root.to_str().unwrap()));
assert_contains_in_order(
&doctor,
&[
"Auto-open",
"supported:",
"scope:",
"backend:",
"Session Agent",
],
);
assert!(!doctor.contains(" vault:"));
assert!(!doctor.contains("running: unknown"));
let open = run_output_in(bin, &["session"], &vault_root, &agent_root);
assert_success(&open);
let open = String::from_utf8_lossy(&open.stdout);
assert!(open.contains("Session Agent:"));
assert!(open.contains(" enabled:"));
assert!(open.contains(" running:"));
assert!(open.contains("Auto-open:"));
assert!(open.contains(" scope:"));
assert!(open.contains("Default lockbox:"));
assert!(open.contains("Open lockboxes:"));
assert!(open.contains("none"));
let stop = run_output_in(bin, &["session", "stop"], &vault_root, &agent_root);
assert_success(&stop);
assert!(String::from_utf8_lossy(&stop.stdout).contains("Session Agent stopped"));
let auto_open = run_output_in(
bin,
&["session", "auto-open", "status", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&auto_open);
let auto_open = String::from_utf8_lossy(&auto_open.stdout);
assert_contains_in_order(
&auto_open,
&[
"supported\t",
"scope\t",
"Vault passphrase stored\t",
"backend\t",
"vault\t",
],
);
let auto_open_default = run_output_in(bin, &["session", "auto-open"], &vault_root, &agent_root);
assert_success(&auto_open_default);
let auto_open_default = String::from_utf8_lossy(&auto_open_default.stdout);
assert!(auto_open_default.contains("supported"));
assert!(auto_open_default.contains("scope"));
}
#[test]
fn doctor_lockbox_reports_closed_metadata_and_open_guidance() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("doctor-lockbox-closed");
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("closed.lbox");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
run_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "create", "--password"],
&vault_root,
&agent_root,
);
run_without_content_key(bin, &["session", "close-all"], &vault_root, &agent_root);
let doctor = run_output_without_lockbox_password(
bin,
&[lockbox.to_str().unwrap(), "doctor"],
&vault_root,
&agent_root,
)
.output()
.unwrap();
assert_success(&doctor);
let doctor = String::from_utf8_lossy(&doctor.stdout);
assert!(doctor.contains("Lockbox"));
assert!(doctor.contains(lockbox.to_str().unwrap()));
assert!(doctor.contains("Configured access (public header)"));
assert!(doctor.contains("pass phrase slots: 1"));
assert!(doctor.contains("contact-key slots: 0"));
assert!(doctor.contains("Encrypted content"));
assert!(doctor.contains("state: not checked (lockbox is closed)"));
assert!(doctor.contains("next: open the lockbox, then run doctor again to check its health:"));
assert!(doctor.contains(&format!("lbx {} open", lockbox.display())));
assert!(doctor.contains(&format!("lbx {} doctor", lockbox.display())));
assert!(!doctor.contains("no cached content key"));
assert!(!doctor.contains("key directory:"));
let verbose_doctor = run_output_without_lockbox_password(
bin,
&[lockbox.to_str().unwrap(), "doctor", "--verbose"],
&vault_root,
&agent_root,
)
.output()
.unwrap();
assert_success(&verbose_doctor);
let verbose_doctor = String::from_utf8_lossy(&verbose_doctor.stdout);
assert!(verbose_doctor.contains("Technical details"));
assert!(verbose_doctor.contains("lockbox id:"));
assert!(verbose_doctor.contains("physical bytes:"));
assert!(verbose_doctor.contains("primary header: ok"));
assert!(verbose_doctor.contains("key directory: generation"));
}
#[test]
fn doctor_lockbox_adds_open_checks_when_opened() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("doctor-lockbox-open");
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("open.lbox");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
run_without_content_key(
bin,
&[
lockbox.to_str().unwrap(),
"create",
"--description",
"Initial encrypted purpose",
],
&vault_root,
&agent_root,
);
let open = run_output_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "open"],
&vault_root,
&agent_root,
);
if is_session_agent_unavailable(&open) {
eprintln!("skipping doctor open checks: Session Agent unavailable");
return;
}
assert_success(&open);
let initial_description = run_output_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "description", "get"],
&vault_root,
&agent_root,
);
assert_success(&initial_description);
assert_eq!(
String::from_utf8_lossy(&initial_description.stdout).trim(),
"Initial encrypted purpose"
);
let set_description = run_output_without_content_key_with_stdin(
bin,
&[lockbox.to_str().unwrap(), "description", "set", "--stdin"],
&vault_root,
&agent_root,
"Updated encrypted purpose",
);
assert_success(&set_description);
let remembered = run_output_without_content_key(
bin,
&[
"vault",
"lockbox",
"list",
"--with-description",
"--format",
"json",
],
&vault_root,
&agent_root,
);
assert_success(&remembered);
assert!(String::from_utf8_lossy(&remembered.stdout)
.contains("\"description\":\"Updated encrypted purpose\""));
let doctor = run_output_without_lockbox_password(
bin,
&[lockbox.to_str().unwrap(), "doctor"],
&vault_root,
&agent_root,
)
.output()
.unwrap();
assert_success(&doctor);
let doctor = String::from_utf8_lossy(&doctor.stdout);
assert!(doctor.contains("Encrypted content"));
assert!(doctor.contains("description: Updated encrypted purpose"));
assert!(doctor.contains("state: healthy"));
assert!(!doctor.contains("storage length:"));
assert!(!doctor.contains("partial files:"));
let global_doctor =
run_output_without_lockbox_password(bin, &["doctor"], &vault_root, &agent_root)
.output()
.unwrap();
assert_success(&global_doctor);
let global_doctor = String::from_utf8_lossy(&global_doctor.stdout);
assert!(global_doctor.contains("open lockboxes: 1"));
let clear_description = run_output_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "description", "clear"],
&vault_root,
&agent_root,
);
assert_success(&clear_description);
let missing_description = run_output_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "description", "get"],
&vault_root,
&agent_root,
);
assert!(!missing_description.status.success());
assert!(String::from_utf8_lossy(&missing_description.stderr).contains("lockbox description"));
}
#[test]
fn cli_env_rename_and_visualize_flow() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir();
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("test.lbox");
let source = dir.join("source.txt");
fs::write(&source, "alpha").unwrap();
run(
bin,
&[
lockbox.to_str().unwrap(),
"add",
source.to_str().unwrap(),
"--to",
"/docs/a.txt",
],
);
let renamed = run_output(
bin,
&[lockbox.to_str().unwrap(), "move", "/docs", "/archive/docs"],
);
assert_success(&renamed);
assert!(String::from_utf8_lossy(&renamed.stdout).contains("Moved /docs to /archive/docs."));
let var_set = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"var",
"set",
"DATABASE_URL",
"postgres://localhost/app",
],
);
assert_success(&var_set);
assert!(String::from_utf8_lossy(&var_set.stdout).contains("Variable set: /DATABASE_URL"));
let listing = run_output(bin, &[lockbox.to_str().unwrap(), "list", "/archive/docs"]);
assert_success(&listing);
let listing = String::from_utf8_lossy(&listing.stdout);
assert!(listing.contains("a.txt"));
assert!(!listing.contains("/archive/docs/a.txt"));
assert!(!listing.contains("DATABASE_URL"));
let env_get = run_output(
bin,
&[lockbox.to_str().unwrap(), "var", "get", "DATABASE_URL"],
);
assert_success(&env_get);
assert_eq!(
String::from_utf8_lossy(&env_get.stdout).trim(),
"postgres://localhost/app"
);
let visualize = run_output(bin, &[lockbox.to_str().unwrap(), "visualize"]);
assert_success(&visualize);
let visualize = String::from_utf8_lossy(&visualize.stdout);
assert!(visualize.contains("Lockbox"));
assert!(visualize.contains("summary:"));
assert!(visualize.contains("files: 1"));
assert!(visualize.contains("variables: 1"));
assert!(visualize.contains("pages:"));
assert!(visualize.contains("----------------------------------------"));
assert!(!visualize.contains("DATABASE_URL"));
assert!(!visualize.contains("/archive/docs/a.txt"));
assert!(visualize.contains("recovery scan:"));
let vault_public = dir.join("default.pub");
run(bin, &["vault", "profile", "create", "default"]);
run(
bin,
&[
"vault",
"profile",
"export",
vault_public.to_str().unwrap(),
"--name",
"default",
],
);
let contact_fingerprint = encode_hex(&public_key_fingerprint(
&import_public_key(&fs::read(&vault_public).unwrap()).unwrap(),
));
run(
bin,
&[
"vault",
"contact",
"import",
"default",
vault_public.to_str().unwrap(),
"--fingerprint",
&contact_fingerprint,
"--fingerprint-channel",
"phone-call-to-owner",
],
);
let profile_list = run_output(bin, &["vault", "profile", "list", "--format", "tsv"]);
assert_success(&profile_list);
assert!(String::from_utf8_lossy(&profile_list.stdout).contains("default"));
let contact_list = run_output(bin, &["vault", "contact", "list", "--format", "tsv"]);
assert_success(&contact_list);
assert!(String::from_utf8_lossy(&contact_list.stdout).contains("default"));
let vault_file = unique_dir().join("vault").join("local-vault.lbox");
let vault_bytes = fs::read(vault_file).unwrap();
assert!(!String::from_utf8_lossy(&vault_bytes).contains("test-key"));
let public_export = dir.join("exported.pub");
run(
bin,
&[
"vault",
"profile",
"export",
public_export.to_str().unwrap(),
"--name",
"default",
],
);
assert!(public_export.exists());
assert!(String::from_utf8_lossy(&fs::read(&public_export).unwrap())
.contains("BEGIN LOCKBOX PUBLIC KEY"));
let public_jwk = dir.join("exported.jwk");
run(
bin,
&[
"vault",
"profile",
"export",
"--format",
"jwk",
public_jwk.to_str().unwrap(),
"--name",
"default",
],
);
let public_jwk_text = String::from_utf8_lossy(&fs::read(&public_jwk).unwrap()).to_string();
assert!(public_jwk_text.contains("\"alg\": \"X25519-ML-KEM-768\""));
run(bin, &["vault", "contact", "remove", "default"]);
run(bin, &["vault", "profile", "remove", "--force", "default"]);
let profile_list = run_output(bin, &["vault", "profile", "list"]);
assert_success(&profile_list);
assert!(!String::from_utf8_lossy(&profile_list.stdout).contains("default"));
let contact_list = run_output(bin, &["vault", "contact", "list"]);
assert_success(&contact_list);
assert!(!String::from_utf8_lossy(&contact_list.stdout).contains("default"));
let doctor = run_output(bin, &["doctor"]);
assert_success(&doctor);
let doctor = String::from_utf8_lossy(&doctor.stdout);
assert!(doctor.contains("Local vault"));
assert!(doctor.contains("Auto-open"));
assert!(doctor.contains("local-vault.lbox"));
}
#[test]
fn list_commands_support_table_tsv_and_json_formats() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("output-formats");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("formats.lbox");
let source = dir.join("source.txt");
fs::write(&source, "alpha").unwrap();
let first_add = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"add",
source.to_str().unwrap(),
"--to",
"/docs/a.txt",
],
);
assert_success(&first_add);
assert!(String::from_utf8_lossy(&first_add.stdout).contains("Added 1 file to"));
let table = run_output(bin, &[lockbox.to_str().unwrap(), "list"]);
assert_success(&table);
let table = String::from_utf8_lossy(&table.stdout);
assert!(table.lines().next().unwrap_or("").contains("kind"));
assert!(table.lines().next().unwrap_or("").contains("name"));
assert!(table.contains("docs/"));
assert!(!table.contains("/docs/a.txt"));
let recursive = run_output(bin, &[lockbox.to_str().unwrap(), "list", "--recursive"]);
assert_success(&recursive);
let recursive = String::from_utf8_lossy(&recursive.stdout);
assert!(recursive.lines().next().unwrap_or("").contains("path"));
assert!(recursive.contains("/docs/a.txt"));
let glob = run_output(bin, &[lockbox.to_str().unwrap(), "list", "/docs/*.txt"]);
assert_success(&glob);
let glob = String::from_utf8_lossy(&glob.stdout);
assert!(glob.lines().next().unwrap_or("").contains("path"));
assert!(glob.contains("/docs/a.txt"));
let tsv = run_output(bin, &[lockbox.to_str().unwrap(), "list", "--format", "tsv"]);
assert_success(&tsv);
assert!(String::from_utf8_lossy(&tsv.stdout).contains("directory\t-\tdocs/"));
let json = run_output(
bin,
&[lockbox.to_str().unwrap(), "list", "--format", "json"],
);
assert_success(&json);
assert!(String::from_utf8_lossy(&json.stdout)
.contains("{\"kind\":\"directory\",\"len\":\"-\",\"name\":\"docs/\"}"));
let recursive_json = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"list",
"--recursive",
"--format",
"json",
],
);
assert_success(&recursive_json);
assert!(String::from_utf8_lossy(&recursive_json.stdout)
.contains("{\"kind\":\"file\",\"len\":\"5\",\"path\":\"/docs/a.txt\"}"));
}
#[test]
fn recover_reports_and_writes_recovered_lockbox() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("recover-write");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let damaged = dir.join("damaged.lbox");
let recovered = dir.join("recovered.lbox");
let source = dir.join("source.txt");
fs::write(&source, "alpha").unwrap();
run(
bin,
&[
damaged.to_str().unwrap(),
"add",
source.to_str().unwrap(),
"--to",
"/docs/a.txt",
],
);
let mut bytes = fs::read(&damaged).unwrap();
bytes[0] ^= 0xff;
fs::write(&damaged, bytes).unwrap();
let report = run_output(
bin,
&[
damaged.to_str().unwrap(),
"doctor",
"recover",
"--dry-run",
"--format",
"json",
],
);
assert_success(&report);
assert!(String::from_utf8_lossy(&report.stdout).contains("\"field\":\"intact_file_count\""));
let default_recovered = dir.join("damaged.recovered.lbox");
let default_output = run_output(bin, &[damaged.to_str().unwrap(), "doctor", "recover"]);
assert_success(&default_output);
assert!(damaged.exists());
assert!(default_recovered.exists());
assert!(String::from_utf8_lossy(&default_output.stdout)
.contains(default_recovered.to_str().unwrap()));
fs::remove_file(&default_recovered).unwrap();
let output = run_output(
bin,
&[
damaged.to_str().unwrap(),
"doctor",
"recover",
"--output",
recovered.to_str().unwrap(),
"--format",
"tsv",
],
);
assert_success(&output);
assert!(recovered.exists());
assert!(String::from_utf8_lossy(&output.stdout).contains("output\t"));
let listing = run_output(
bin,
&[
recovered.to_str().unwrap(),
"list",
"--recursive",
"--format",
"tsv",
],
);
assert_success(&listing);
assert!(String::from_utf8_lossy(&listing.stdout).contains("/docs/a.txt"));
let in_place = dir.join("damaged-in-place.lbox");
let in_place_backup = dir.join("damaged-in-place.lbox.damaged");
fs::copy(&damaged, &in_place).unwrap();
let in_place_output = run_output(
bin,
&[
in_place.to_str().unwrap(),
"doctor",
"recover",
"--output",
in_place.to_str().unwrap(),
"--overwrite",
"--format",
"tsv",
],
);
assert_success(&in_place_output);
assert!(in_place.exists());
assert!(in_place_backup.exists());
let in_place_output = String::from_utf8_lossy(&in_place_output.stdout);
assert!(in_place_output.contains("output\t"));
assert!(in_place_output.contains("damaged_original\t"));
let listing = run_output(
bin,
&[
in_place.to_str().unwrap(),
"list",
"--recursive",
"--format",
"tsv",
],
);
assert_success(&listing);
assert!(String::from_utf8_lossy(&listing.stdout).contains("/docs/a.txt"));
}
#[test]
fn doctor_recover_detects_and_completes_interrupted_cleanup() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("recover-transaction");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("interrupted.lbox");
revault_lockbox_api::write_interrupted_transaction_fixture(&lockbox, b"test-key").unwrap();
let diagnosis = run_output(bin, &[lockbox.to_str().unwrap(), "doctor"]);
assert_success(&diagnosis);
let diagnosis_stdout = String::from_utf8_lossy(&diagnosis.stdout);
assert!(diagnosis_stdout.contains("state: cleanup required"));
assert!(diagnosis_stdout.contains(&format!(
"preview: lbx {} doctor recover --dry-run",
lockbox.display()
)));
assert!(diagnosis_stdout.contains(&format!(
"recover: lbx {} doctor recover",
lockbox.display()
)));
let preview = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"doctor",
"recover",
"--dry-run",
"--format",
"json",
"--quiet",
],
);
assert_success(&preview);
assert!(preview.stderr.is_empty());
let preview = String::from_utf8_lossy(&preview.stdout);
assert!(preview.contains("\"field\":\"operation\""));
assert!(preview.contains("\"value\":\"complete_pending_cleanup\""));
let still_pending = run_output(bin, &[lockbox.to_str().unwrap(), "doctor"]);
assert_success(&still_pending);
assert!(String::from_utf8_lossy(&still_pending.stdout).contains("state: cleanup required"));
let recovered = run_output(bin, &[lockbox.to_str().unwrap(), "doctor", "recover"]);
assert_success(&recovered);
let stderr = String::from_utf8_lossy(&recovered.stderr);
assert!(stderr.contains("Lockbox cleanup is required"));
assert!(stderr.contains("Lockbox cleanup complete"));
assert!(!stderr.contains("ranges"));
let listing = run_output(bin, &[lockbox.to_str().unwrap(), "list", "--recursive"]);
assert_success(&listing);
let listing = String::from_utf8_lossy(&listing.stdout);
assert!(listing.contains("/docs/keep.txt"));
assert!(!listing.contains("/docs/remove.txt"));
}
#[test]
fn ordinary_write_capable_open_completes_interrupted_cleanup_automatically() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("automatic-cleanup");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("interrupted.lbox");
revault_lockbox_api::write_interrupted_transaction_fixture(&lockbox, b"test-key").unwrap();
let listing = run_output(bin, &[lockbox.to_str().unwrap(), "list", "--recursive"]);
assert_success(&listing);
let cleanup = String::from_utf8_lossy(&listing.stderr);
assert!(cleanup.contains("Lockbox cleanup is required"));
assert!(cleanup.contains("Lockbox cleanup complete"));
assert!(!cleanup.contains("ranges"));
let listing = String::from_utf8_lossy(&listing.stdout);
assert!(listing.contains("/docs/keep.txt"));
assert!(!listing.contains("/docs/remove.txt"));
let status = revault_lockbox_api::Lockbox::inspect_transaction_recovery(
&lockbox,
revault_lockbox_api::LockboxOpen::ContentKey(
revault_lockbox_api::SecretVec::try_from_slice(b"test-key").unwrap(),
),
)
.unwrap();
assert!(status.is_none());
}
#[test]
fn content_key_create_does_not_mirror_empty_key_directory() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("raw-create");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("raw.lbox");
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
run_in(
bin,
&[lockbox.to_str().unwrap(), "create"],
&vault_root,
&agent_root,
);
assert!(lockbox.exists());
let listing = run_output_in(
bin,
&[lockbox.to_str().unwrap(), "list"],
&vault_root,
&agent_root,
);
assert_success(&listing);
assert_eq!(String::from_utf8_lossy(&listing.stdout).trim(), "empty");
assert!(vault_root.join("local-vault.lbox").exists());
let remembered = run_output_in(bin, &["vault", "lockbox", "list"], &vault_root, &agent_root);
assert_success(&remembered);
assert_eq!(String::from_utf8_lossy(&remembered.stdout).trim(), "empty");
}
#[test]
fn vault_lockbox_remembers_an_existing_lockbox_by_absolute_path() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("remember-existing");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("wayward.lbox");
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
run_in(
bin,
&[lockbox.to_str().unwrap(), "create"],
&vault_root,
&agent_root,
);
let remembered = run_output_in(
bin,
&["vault", "lockbox", "remember", lockbox.to_str().unwrap()],
&vault_root,
&agent_root,
);
assert_success(&remembered);
let listing = run_output_in(
bin,
&["vault", "lockbox", "list", "--format", "json"],
&vault_root,
&agent_root,
);
assert_success(&listing);
let canonical = lockbox.canonicalize().unwrap();
assert!(String::from_utf8_lossy(&listing.stdout).contains(canonical.to_str().unwrap()));
let forgotten = run_output_in(
bin,
&["vault", "lockbox", "forget", lockbox.to_str().unwrap()],
&vault_root,
&agent_root,
);
assert_success(&forgotten);
let listing = run_output_in(
bin,
&["vault", "lockbox", "list", "--format", "json"],
&vault_root,
&agent_root,
);
assert_success(&listing);
assert!(!String::from_utf8_lossy(&listing.stdout).contains(canonical.to_str().unwrap()));
}
#[test]
fn create_refuses_to_overwrite_existing_lockbox() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("create-no-overwrite");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("safe.lbox");
run(bin, &[lockbox.to_str().unwrap(), "create"]);
let original = fs::read(&lockbox).unwrap();
let output = run_output(bin, &[lockbox.to_str().unwrap(), "create"]);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("already exists"));
assert_eq!(fs::read(&lockbox).unwrap(), original);
}
#[test]
fn create_defaults_lbox_extension_and_reports_before_prompting() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("create-extension");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
let requested = dir.join("project");
let created = dir.join("project.lbox");
let output = run_output_without_content_key(
bin,
&[requested.to_str().unwrap(), "create"],
&vault_root,
&agent_root,
);
assert_success(&output);
assert!(created.exists());
assert!(!requested.exists());
assert!(String::from_utf8_lossy(&output.stdout).contains("Creating lockbox:"));
assert!(String::from_utf8_lossy(&output.stdout).contains("project.lbox"));
let listing = run_output_without_lockbox_password(
bin,
&[created.to_str().unwrap(), "list"],
&vault_root,
&agent_root,
)
.output()
.unwrap();
if is_session_agent_unavailable(&listing) {
eprintln!("skipping create extension listing assertion: Session Agent unavailable");
return;
}
assert_success(&listing);
assert_eq!(String::from_utf8_lossy(&listing.stdout).trim(), "empty");
let original = fs::read(&created).unwrap();
let duplicate = run_output_without_content_key(
bin,
&[requested.to_str().unwrap(), "create"],
&vault_root,
&agent_root,
);
assert!(!duplicate.status.success());
assert!(String::from_utf8_lossy(&duplicate.stderr).contains("already exists"));
assert!(!String::from_utf8_lossy(&duplicate.stdout).contains("Creating lockbox"));
assert_eq!(fs::read(&created).unwrap(), original);
}
#[test]
fn add_can_default_destination_and_list_recursively() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("add-default-path");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("files.lbox");
let source_file = dir.join("alpha.txt");
fs::write(&source_file, "alpha").unwrap();
let second_file = dir.join("beta.txt");
fs::write(&second_file, "beta").unwrap();
let source_dir = dir.join("src");
fs::create_dir_all(&source_dir).unwrap();
fs::write(source_dir.join("one.txt"), "one").unwrap();
fs::write(source_dir.join("two.txt"), "two").unwrap();
fs::write(source_dir.join("ignored.tmp"), "ignored").unwrap();
let second_source_dir = dir.join("assets");
fs::create_dir_all(&second_source_dir).unwrap();
fs::write(second_source_dir.join("logo.txt"), "logo").unwrap();
let first_add = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"add",
source_file.to_str().unwrap(),
],
);
assert_success(&first_add);
assert!(String::from_utf8_lossy(&first_add.stdout).contains("Added 1 file to"));
let protected_existing = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"add",
source_file.to_str().unwrap(),
],
);
assert!(!protected_existing.status.success());
assert!(String::from_utf8_lossy(&protected_existing.stderr).contains("already exists"));
fs::write(&source_file, "alpha updated").unwrap();
let overwritten = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"add",
source_file.to_str().unwrap(),
"--overwrite",
],
);
assert_success(&overwritten);
assert!(String::from_utf8_lossy(&overwritten.stdout).contains("Replaced 1 file in"));
let updated = run_output(bin, &[lockbox.to_str().unwrap(), "cat", "alpha.txt"]);
assert_success(&updated);
assert_eq!(String::from_utf8_lossy(&updated.stdout), "alpha updated");
run(
bin,
&[
lockbox.to_str().unwrap(),
"add",
source_file.to_str().unwrap(),
"--to",
"some/path/alpha.txt",
],
);
let ambiguous_multi_destination = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"add",
source_file.to_str().unwrap(),
second_file.to_str().unwrap(),
"--to",
"bundle",
],
);
assert!(!ambiguous_multi_destination.status.success());
assert!(String::from_utf8_lossy(&ambiguous_multi_destination.stderr)
.contains("--to must end with / when adding multiple sources"));
let multi_add = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"add",
source_file.to_str().unwrap(),
second_file.to_str().unwrap(),
"--to",
"bundle/",
],
);
assert_success(&multi_add);
assert!(String::from_utf8_lossy(&multi_add.stdout).contains("Added 2 files to"));
let directory_without_recursive = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"add",
source_dir.to_str().unwrap(),
"--to",
"/copy",
],
);
assert!(!directory_without_recursive.status.success());
assert!(String::from_utf8_lossy(&directory_without_recursive.stderr).contains("--recursive"));
run(
bin,
&[
lockbox.to_str().unwrap(),
"add",
"--recursive",
source_dir.to_str().unwrap(),
"--to",
"/copy",
"--include",
"*.txt",
"--exclude",
"*.tmp",
],
);
let progress_vault_root = unique_dir().join("vault");
let progress_agent_root = unique_dir().join("agent");
let progress = Command::new(bin)
.args([
lockbox.to_str().unwrap(),
"add",
"--recursive",
source_dir.to_str().unwrap(),
"--to",
"/progress",
"--exclude",
"*.tmp",
])
.env("LOCKBOX_KEY", "test-key")
.env("LOCKBOX_VAULT_PASSWORD", "test-vault-password")
.env("LOCKBOX_SESSION_AGENT_DIR", &progress_agent_root)
.env(
"LOCKBOX_SESSION_AGENT_LOG",
agent_log_path(&progress_agent_root),
)
.env("LOCKBOX_VAULT_DIR", &progress_vault_root)
.env("LOCKBOX_ADD_PROGRESS", "always")
.output()
.unwrap();
assert_success(&progress);
let progress_stderr = String::from_utf8_lossy(&progress.stderr);
assert!(progress_stderr.contains("\rAdding: "));
assert!(progress_stderr.contains("two.txt"));
assert_eq!(
progress_stderr.matches('\n').count(),
1,
"{progress_stderr:?}"
);
let quiet_progress = Command::new(bin)
.args([
lockbox.to_str().unwrap(),
"add",
"--recursive",
source_dir.to_str().unwrap(),
"--to",
"/quiet",
"--exclude",
"*.tmp",
"--quiet",
])
.env("LOCKBOX_KEY", "test-key")
.env("LOCKBOX_VAULT_PASSWORD", "test-vault-password")
.env("LOCKBOX_SESSION_AGENT_DIR", &progress_agent_root)
.env(
"LOCKBOX_SESSION_AGENT_LOG",
agent_log_path(&progress_agent_root),
)
.env("LOCKBOX_VAULT_DIR", &progress_vault_root)
.env("LOCKBOX_ADD_PROGRESS", "always")
.output()
.unwrap();
assert_success(&quiet_progress);
assert!(quiet_progress.stderr.is_empty());
let quiet_stored = run_output(bin, &[lockbox.to_str().unwrap(), "cat", "/quiet/two.txt"]);
assert_success(&quiet_stored);
assert_eq!(
quiet_stored.stdout,
fs::read(source_dir.join("two.txt")).unwrap()
);
let listing = run_output(bin, &[lockbox.to_str().unwrap(), "ls"]);
assert_success(&listing);
let listing = String::from_utf8_lossy(&listing.stdout);
assert!(listing.contains("alpha.txt"));
assert!(listing.contains("copy/"));
assert!(!listing.contains("/copy/one.txt"));
let recursive = run_output(bin, &[lockbox.to_str().unwrap(), "ls", "--recursive"]);
assert_success(&recursive);
assert!(!String::from_utf8_lossy(&recursive.stdout).contains("/copy/ignored.tmp"));
let recursive = String::from_utf8_lossy(&recursive.stdout);
assert!(recursive.contains("/alpha.txt"));
assert!(recursive.contains("/some/path/alpha.txt"));
assert!(recursive.contains("/bundle/alpha.txt"));
assert!(recursive.contains("/bundle/beta.txt"));
assert!(recursive.contains("/copy/one.txt"));
assert!(recursive.contains("/copy/two.txt"));
let mixed_lockbox = dir.join("mixed.lbox");
let mixed_add = run_output(
bin,
&[
mixed_lockbox.to_str().unwrap(),
"add",
"--recursive",
source_dir.to_str().unwrap(),
second_source_dir.to_str().unwrap(),
second_file.to_str().unwrap(),
],
);
assert_success(&mixed_add);
let mixed_listing = run_output(bin, &[mixed_lockbox.to_str().unwrap(), "ls", "--recursive"]);
assert_success(&mixed_listing);
let mixed_listing = String::from_utf8_lossy(&mixed_listing.stdout);
assert!(mixed_listing.contains("/src/one.txt"));
assert!(mixed_listing.contains("/assets/logo.txt"));
assert!(mixed_listing.contains("/beta.txt"));
let nested = run_output(bin, &[lockbox.to_str().unwrap(), "ls", "/some/path"]);
assert_success(&nested);
assert!(String::from_utf8_lossy(&nested.stdout).contains("alpha.txt"));
let relative_cat = run_output(
bin,
&[lockbox.to_str().unwrap(), "cat", "some/path/alpha.txt"],
);
assert_success(&relative_cat);
assert_eq!(
String::from_utf8_lossy(&relative_cat.stdout),
"alpha updated"
);
}
#[test]
fn add_recursive_dot_imports_the_current_directory_contents() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("add-recursive-dot");
let _ = fs::remove_dir_all(&dir);
let source_dir = dir.join("source");
fs::create_dir_all(source_dir.join("nested")).unwrap();
fs::write(source_dir.join("visible.txt"), "visible").unwrap();
fs::write(source_dir.join(".hidden"), "hidden").unwrap();
fs::write(source_dir.join("nested/child.txt"), "child").unwrap();
let lockbox = source_dir.join("dot.lbox");
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let add = Command::new(bin)
.current_dir(&source_dir)
.args([lockbox.to_str().unwrap(), "add", "--recursive", "."])
.env("LOCKBOX_KEY", "test-key")
.env("LOCKBOX_VAULT_PASSWORD", "test-vault-password")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.output()
.unwrap();
assert_success(&add);
let listing = run_output_in(
bin,
&[lockbox.to_str().unwrap(), "ls", "--recursive"],
&vault_root,
&agent_root,
);
assert_success(&listing);
let listing = String::from_utf8_lossy(&listing.stdout);
assert!(listing.contains("/visible.txt"));
assert!(listing.contains("/.hidden"));
assert!(listing.contains("/nested/child.txt"));
assert!(!listing.contains("dot.lbox"));
assert!(!listing.contains(".dot.lbox.lock"));
let stored = run_output_in(
bin,
&[lockbox.to_str().unwrap(), "cat", "/nested/child.txt"],
&vault_root,
&agent_root,
);
assert_success(&stored);
assert_eq!(
stored.stdout,
fs::read(source_dir.join("nested/child.txt")).unwrap()
);
}
#[test]
fn access_subcommands_manage_lockbox_access() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("access-subcommand");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("share.lbox");
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let source = dir.join("source.txt");
let public_key = dir.join("access.pub");
let second_public_key = dir.join("access2.pub");
fs::write(&source, "alpha").unwrap();
run_in(bin, &["vault", "init"], &vault_root, &agent_root);
run_in(
bin,
&[lockbox.to_str().unwrap(), "add", source.to_str().unwrap()],
&vault_root,
&agent_root,
);
run_in(
bin,
&["vault", "profile", "create", "sharee"],
&vault_root,
&agent_root,
);
let default_export = run_output_in(
bin,
&[
"vault",
"profile",
"export",
public_key.to_str().unwrap(),
"--name",
"sharee",
],
&vault_root,
&agent_root,
);
assert_success(&default_export);
assert!(String::from_utf8_lossy(&default_export.stdout).contains("public_key_fingerprint="));
run_in(
bin,
&["vault", "profile", "create", "sharee2"],
&vault_root,
&agent_root,
);
let second_export = run_output_in(
bin,
&[
"vault",
"profile",
"export",
second_public_key.to_str().unwrap(),
"--name",
"sharee2",
],
&vault_root,
&agent_root,
);
assert_success(&second_export);
let second_contact_key = import_public_key(&fs::read(&second_public_key).unwrap()).unwrap();
let second_contact_fingerprint = encode_hex(&public_key_fingerprint(&second_contact_key));
run_in(
bin,
&[
"vault",
"contact",
"import",
"sharee",
second_public_key.to_str().unwrap(),
"--fingerprint",
&second_contact_fingerprint,
"--fingerprint-channel",
"phone-call-to-owner",
],
&vault_root,
&agent_root,
);
let ambiguous = run_output_in(
bin,
&[lockbox.to_str().unwrap(), "access", "grant", "sharee"],
&vault_root,
&agent_root,
);
assert!(!ambiguous.status.success());
let ambiguous = String::from_utf8_lossy(&ambiguous.stderr);
assert!(ambiguous.contains("ambiguous access target: sharee"));
assert!(ambiguous.contains("profile:sharee"));
assert!(ambiguous.contains("contact:sharee"));
let path_only = run_output_in(
bin,
&[
lockbox.to_str().unwrap(),
"access",
"grant",
public_key.to_str().unwrap(),
],
&vault_root,
&agent_root,
);
assert!(!path_only.status.success());
assert!(String::from_utf8_lossy(&path_only.stderr)
.contains("public key files require a contact name"));
run_in(
bin,
&[
lockbox.to_str().unwrap(),
"access",
"grant",
"external",
public_key.to_str().unwrap(),
],
&vault_root,
&agent_root,
);
run_in(
bin,
&[
lockbox.to_str().unwrap(),
"access",
"grant",
"profile:sharee",
],
&vault_root,
&agent_root,
);
run_in(
bin,
&[
lockbox.to_str().unwrap(),
"access",
"grant",
"contact:sharee",
],
&vault_root,
&agent_root,
);
run_in(bin, &["session", "close-all"], &vault_root, &agent_root);
let opened_with_key = run_output_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "open-key", "sharee"],
&vault_root,
&agent_root,
);
if !is_session_agent_unavailable(&opened_with_key) {
assert_success(&opened_with_key);
assert!(String::from_utf8_lossy(&opened_with_key.stdout).contains("Lockbox opened:"));
}
let access = run_output_in(
bin,
&[lockbox.to_str().unwrap(), "access", "ls", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&access);
let access = String::from_utf8_lossy(&access.stdout);
assert!(access.lines().any(|line| !line.trim().is_empty()));
assert!(access.contains("\texternal\tContact\t"));
assert!(access.contains("\tprofile:sharee\tContact\t"));
assert!(access.contains("\tcontact:sharee\tContact\t"));
let access_json = run_output_in(
bin,
&[
lockbox.to_str().unwrap(),
"access",
"ls",
"--format",
"json",
],
&vault_root,
&agent_root,
);
assert_success(&access_json);
let access_json = String::from_utf8_lossy(&access_json.stdout);
assert!(access_json.contains("\"name\":\"external\""));
assert!(access_json.contains("\"name\":\"profile:sharee\""));
assert!(access_json.contains("\"name\":\"contact:sharee\""));
assert!(access_json.contains("\"owner\":\""));
assert!(!access_json.contains("\"owner\":\"-\""));
assert!(access_json.contains("\"owner_signed\":\"yes\""));
assert!(access_json.contains("\"created\":\""));
assert!(access_json.contains("\"updated\":\""));
let ambiguous = run_output_in(
bin,
&[lockbox.to_str().unwrap(), "access", "revoke", "sharee"],
&vault_root,
&agent_root,
);
assert!(!ambiguous.status.success());
assert!(String::from_utf8_lossy(&ambiguous.stderr).contains("multiple local access labels"));
run_in(
bin,
&[
lockbox.to_str().unwrap(),
"access",
"revoke",
"external",
"profile:sharee",
],
&vault_root,
&agent_root,
);
let access = run_output_without_lockbox_password(
bin,
&[lockbox.to_str().unwrap(), "access", "ls", "--format", "tsv"],
&vault_root,
&agent_root,
)
.output()
.unwrap();
assert_success(&access);
let access = String::from_utf8_lossy(&access.stdout);
assert!(!access.contains("external"));
assert!(!access.contains("profile:sharee"));
assert!(access.contains("contact:sharee"));
let grant = run_output_without_lockbox_password(
bin,
&[
lockbox.to_str().unwrap(),
"access",
"grant",
"profile:sharee",
],
&vault_root,
&agent_root,
)
.output()
.unwrap();
assert_success(&grant);
let access = run_output_without_lockbox_password(
bin,
&[lockbox.to_str().unwrap(), "access", "ls", "--format", "tsv"],
&vault_root,
&agent_root,
)
.output()
.unwrap();
assert_success(&access);
let access = String::from_utf8_lossy(&access.stdout);
assert!(access.contains("profile:sharee"));
assert!(access.contains("contact:sharee"));
let slot_id = access
.lines()
.find(|line| !line.trim().is_empty())
.and_then(|line| line.split('\t').next())
.expect("access slot id");
let revoke = run_output_without_lockbox_password(
bin,
&[lockbox.to_str().unwrap(), "access", "revoke", slot_id],
&vault_root,
&agent_root,
)
.output()
.unwrap();
assert_success(&revoke);
}
#[test]
fn password_create_requires_explicit_vault_init() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("create-vault-init");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let lockbox = dir.join("password.lbox");
let create_without_vault = run_output_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "create", "--password"],
&vault_root,
&agent_root,
);
assert!(!create_without_vault.status.success());
assert!(String::from_utf8_lossy(&create_without_vault.stderr)
.contains("local vault is not initialized"));
assert!(!lockbox.exists());
let init = run_output_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
assert_success(&init);
let init = String::from_utf8_lossy(&init.stdout);
assert!(init.contains("Create the local reVault vault."));
assert!(init.contains("Stores:\n - profiles and contacts"));
assert!(init.contains(" - key-directory backups for published lockboxes\n\n"));
assert!(!init.contains("Set a vault password."));
assert!(!init.contains("The vault stores profiles, contacts, and"));
assert!(!init.contains("Choose a vault password you can remember or store securely."));
assert!(!init.contains("Choose a vault password you can back up safely."));
assert!(init.contains("Vault created successfully."));
assert!(init.contains("Directory:\n "));
assert!(init.contains("Profile: default"));
assert!(init.contains("Default forms: 7"));
assert!(
init.contains("Store the following private keys and your Vault passphrase somewhere safe.")
);
assert!(init.contains(
"Anyone with the profile private key can open lockboxes granted to this profile."
));
assert!(init
.contains("Anyone with the signing private key can sign lockbox changes as this profile."));
assert!(init.contains("Profile backup:"));
assert!(init.contains("Public key fingerprint:"));
assert!(init.contains("Profile private key:"));
assert!(init.contains("-----BEGIN LOCKBOX PRIVATE KEY-----"));
assert!(init.contains("Owner signing private key record (hex):"));
assert!(init.contains("Owner signing private key record (hex):\n4c42583153505256"));
assert!(init.contains(
"Passphrase reminder:\n Store the Vault passphrase somewhere safe.\n If it is lost, reVault cannot recover this Vault."
));
assert!(!init.contains("Path:"));
assert!(!init.contains("Default profile:\n default"));
assert!(!init.contains("Created default profile: default"));
assert!(!init.contains("Password reminder:"));
assert!(!init.contains("Store the vault password somewhere safe."));
assert!(!init.contains("Keep the vault password somewhere safe."));
assert!(!init.contains("Record your vault password in a secure place"));
assert!(!init.contains("Back up your vault password before storing important keys."));
assert!(!init.contains("--- reVault profile public key backup"));
assert!(!init.contains("--- reVault profile private key backup"));
assert_contains_in_order(
&init,
&[
"Create the local reVault vault.",
"Vault created successfully.",
"Directory:",
"Profile: default",
"Default forms: 7",
"Store the following private keys and your Vault passphrase somewhere safe.",
"Profile backup:",
"Public key fingerprint:",
"Profile private key:",
"-----BEGIN LOCKBOX PRIVATE KEY-----",
"Owner signing private key record (hex):",
"4c42583153505256",
"Passphrase reminder:",
"If it is lost, reVault cannot recover this Vault.",
],
);
assert!(!init.contains("-----BEGIN LOCKBOX PUBLIC KEY-----"));
assert!(!init.contains("Public key:"));
let vault_password = SecretString::try_from_bytes(b"test-vault-password".to_vec()).unwrap();
let vault = VaultDirectory::open_or_create(&vault_root, &vault_password).unwrap();
let profile_private = extract_profile_private_key_backup(&init);
let restored_profile =
import_private_key(SecretVec::try_from_slice(profile_private.as_bytes()).unwrap()).unwrap();
let stored_profile = vault
.load_private_key(VaultDirectory::DEFAULT_KEY_NAME)
.unwrap();
assert_eq!(
restored_profile.private_key_record().unwrap(),
stored_profile.private_key_record().unwrap()
);
let signing_private_hex = extract_owner_signing_private_key_hex(&init);
let signing_private_record =
SecretVec::try_from_slice(&decode_hex(&signing_private_hex).unwrap()).unwrap();
let restored_signing =
OwnerSigningKeyPair::from_private_key_record(signing_private_record).unwrap();
let stored_signing = vault
.load_owner_signing_key(VaultDirectory::DEFAULT_KEY_NAME)
.unwrap();
assert_eq!(
restored_signing.private_key_record().unwrap(),
stored_signing.private_key_record().unwrap()
);
assert!(vault_root.join("local-vault.lbox").exists());
let default_forms = run_output_without_content_key(
bin,
&["vault", "form", "list", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&default_forms);
let default_forms = String::from_utf8_lossy(&default_forms.stdout);
assert!(default_forms.contains("login\t"));
assert!(default_forms.contains("payment-card\t"));
assert!(default_forms.contains("secure-note\t"));
let auto_open = run_output_without_content_key(
bin,
&["session", "auto-open", "status", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&auto_open);
assert!(String::from_utf8_lossy(&auto_open.stdout).contains("scope\tlockboxes"));
let vault_list = run_output_without_content_key(
bin,
&["vault", "profile", "list", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&vault_list);
let vault_list = String::from_utf8_lossy(&vault_list.stdout);
assert!(vault_list.contains("default"));
run_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "create"],
&vault_root,
&agent_root,
);
assert!(lockbox.exists());
}
#[test]
fn vault_beget_creates_an_independent_profile_and_local_contact() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-beget");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let current_password = SecretString::try_from_bytes(b"test-vault-password".to_vec()).unwrap();
VaultDirectory::open_or_create(&vault_root, ¤t_password).unwrap();
let output = Command::new(bin)
.current_dir(&dir)
.args(["vault", "beget", "production"])
.env("LOCKBOX_VAULT_DIR", &vault_root)
.env("LOCKBOX_VAULT_PASSWORD", "test-vault-password")
.env("LOCKBOX_NEW_VAULT_PASSWORD", "production-vault-password")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.output()
.unwrap();
assert_success(&output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Vault created successfully."));
assert!(stdout.contains("Profile:\n production"));
assert!(stdout.contains("Contact:\n production"));
assert!(stdout.contains("No lockbox access has been granted."));
assert!(stdout.contains("lbx access grant <lockbox> contact:production"));
let begotten_path = dir.join("production.vault.lbx");
assert!(begotten_path.is_file());
let begotten_password =
SecretString::try_from_bytes(b"production-vault-password".to_vec()).unwrap();
let begotten = VaultDirectory::open_file(&begotten_path, &begotten_password).unwrap();
assert_eq!(begotten.list_private_keys().unwrap(), vec!["production"]);
let public_key = begotten
.load_private_key("production")
.unwrap()
.public_key();
begotten.load_owner_signing_key("production").unwrap();
drop(begotten);
let current = VaultDirectory::open_or_create(&vault_root, ¤t_password).unwrap();
assert_eq!(
current.load_contact("production").unwrap().to_bytes(),
public_key.to_bytes()
);
assert!(!current.private_key_exists("production").unwrap());
let collision_path = dir.join("collision.vault.lbx");
let collision = Command::new(bin)
.current_dir(&dir)
.args([
"vault",
"beget",
"production",
"--output",
collision_path.to_str().unwrap(),
])
.env("LOCKBOX_VAULT_DIR", &vault_root)
.env("LOCKBOX_VAULT_PASSWORD", "test-vault-password")
.env("LOCKBOX_NEW_VAULT_PASSWORD", "another-production-password")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.output()
.unwrap();
assert!(!collision.status.success());
assert!(
String::from_utf8_lossy(&collision.stderr).contains("Contact already exists: production")
);
assert!(!collision_path.exists());
}
#[test]
fn vault_beget_supports_custom_output_contact_and_no_contact() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-beget-options");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let current_password = SecretString::try_from_bytes(b"test-vault-password".to_vec()).unwrap();
VaultDirectory::open_or_create(&vault_root, ¤t_password).unwrap();
let custom_path = dir.join("nested").join("runner.vault.lbx");
let custom = Command::new(bin)
.args([
"vault",
"beget",
"production",
"--output",
custom_path.to_str().unwrap(),
"--contact-name",
"ci-production",
])
.env("LOCKBOX_VAULT_DIR", &vault_root)
.env("LOCKBOX_VAULT_PASSWORD", "test-vault-password")
.env("LOCKBOX_NEW_VAULT_PASSWORD", "custom-vault-password")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.output()
.unwrap();
assert_success(&custom);
assert!(custom_path.is_file());
let current = VaultDirectory::open_or_create(&vault_root, ¤t_password).unwrap();
assert!(current.contact_exists("ci-production").unwrap());
assert!(!current.contact_exists("production").unwrap());
drop(current);
let no_contact_root = dir.join("missing-current-vault");
let no_contact_path = dir.join("isolated.vault.lbx");
let no_contact = Command::new(bin)
.args([
"vault",
"beget",
"isolated",
"--output",
no_contact_path.to_str().unwrap(),
"--no-contact",
])
.env("LOCKBOX_VAULT_DIR", &no_contact_root)
.env("LOCKBOX_NEW_VAULT_PASSWORD", "isolated-vault-password")
.env("LOCKBOX_SESSION_AGENT_DIR", dir.join("isolated-agent"))
.output()
.unwrap();
assert_success(&no_contact);
assert!(no_contact_path.is_file());
assert!(!no_contact_root.exists());
assert!(String::from_utf8_lossy(&no_contact.stdout).contains("Contact:\n Not created"));
let missing_password_path = dir.join("missing-password.vault.lbx");
let missing_password = Command::new(bin)
.args([
"vault",
"beget",
"missing-password",
"--output",
missing_password_path.to_str().unwrap(),
"--no-contact",
])
.env("LOCKBOX_VAULT_DIR", &no_contact_root)
.env_remove("LOCKBOX_NEW_VAULT_PASSWORD")
.env("LOCKBOX_SESSION_AGENT_DIR", dir.join("isolated-agent"))
.output()
.unwrap();
assert!(!missing_password.status.success());
assert!(String::from_utf8_lossy(&missing_password.stderr)
.contains("set LOCKBOX_NEW_VAULT_PASSWORD"));
assert!(!missing_password_path.exists());
}
#[test]
fn vault_init_rejects_blank_pass_phrase() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("blank-vault-pass-phrase");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let output = Command::new(bin)
.args(["vault", "init"])
.env("LOCKBOX_PASSWORD", "test-lockbox-password")
.env("LOCKBOX_VAULT_PASSWORD", "")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.env_remove("LOCKBOX_KEY")
.env_remove("LOCKBOX_PASSWORD")
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Vault passphrase must be at least 15 characters"),
"{stderr}"
);
assert!(!vault_root.join("local-vault.lbox").exists());
}
#[test]
fn vault_publish_without_profile_email_is_actionable() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("publish-missing-email");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
let publish = run_output_without_content_key(
bin,
&["vault", "profile", "publish"],
&vault_root,
&agent_root,
);
assert!(!publish.status.success());
let stderr = String::from_utf8_lossy(&publish.stderr);
assert!(stderr.contains(
"You may not publish a public key for a Profile that does not have an email address."
));
assert!(stderr.contains("The profile `default` has no email address."));
assert!(stderr.contains("Run `lockbox vault profile email default <email>`."));
assert!(stderr.contains("Then run this command again."));
assert!(!stderr.contains("invalid input"));
assert!(!stderr.contains("Check the supplied value"));
}
#[test]
fn vault_profile_fingerprint_displays_publish_fingerprint() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("profile-fingerprint");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
run_without_content_key(
bin,
&["vault", "profile", "email", "default", "alice@example.test"],
&vault_root,
&agent_root,
);
let output = run_output_without_content_key(
bin,
&["vault", "profile", "fingerprint", "default"],
&vault_root,
&agent_root,
);
assert_success(&output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("profile=default"));
assert!(stdout.contains("email=alice@example.test"));
let fingerprint = stdout
.lines()
.find_map(|line| line.strip_prefix("contact_fingerprint="))
.expect("fingerprint output");
let groups = fingerprint.split('-').collect::<Vec<_>>();
assert_eq!(groups.len(), 5);
assert!(groups.iter().all(|group| group.len() == 4));
assert_eq!(fingerprint, fingerprint.to_ascii_lowercase());
for ambiguous in ['i', 'l', 'o', 'u'] {
assert!(!fingerprint.contains(ambiguous));
}
assert!(stdout.contains("contact_fingerprint_reading="));
assert!(stdout.contains("contact_fingerprint_hex="));
assert!(stdout.contains("fingerprint_purpose=do not send this fingerprint"));
assert!(stdout.contains("fingerprint_security="));
}
#[test]
fn vault_profile_fingerprint_without_email_is_actionable() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("profile-fingerprint-missing-email");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
let output = run_output_without_content_key(
bin,
&["vault", "profile", "fingerprint"],
&vault_root,
&agent_root,
);
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("Cannot calculate the publish fingerprint for `default`"));
assert!(stderr.contains("Run `lockbox vault profile email default <email>`."));
assert!(!stderr.contains("invalid input"));
}
#[test]
fn vault_init_prompt_mentions_minimum_pass_phrase_length() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-pass-phrase-prompt");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let mut child = Command::new(bin)
.args(["vault", "init"])
.env("LOCKBOX_PASSWORD", "test-lockbox-password")
.env_remove("LOCKBOX_VAULT_PASSWORD")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child.stdin.as_mut().unwrap().write_all(b"2\n\n\n").unwrap();
let output = child.wait_with_output().unwrap();
assert!(!output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stdout.contains("Vault passphrase:"));
assert!(stdout.contains("1. Generate a strong passphrase"));
assert!(stdout.contains("2. Enter my own passphrase"));
assert!(stdout.contains("3. Cancel vault init"));
assert!(stdout.contains("New Vault passphrase (minimum 15 characters):"));
assert!(stderr.contains("Vault passphrase must be at least 15 characters"));
assert!(!vault_root.join("local-vault.lbox").exists());
}
#[test]
fn vault_init_generated_pass_phrase_requires_stored_confirmation() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-pass-phrase-generated");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let mut child = Command::new(bin)
.args(["vault", "init"])
.env("LOCKBOX_PASSWORD", "test-lockbox-password")
.env_remove("LOCKBOX_VAULT_PASSWORD")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child.stdin.as_mut().unwrap().write_all(b"\n\n").unwrap();
let output = child.wait_with_output().unwrap();
assert!(!output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stdout.contains("Generated Vault passphrase:"));
assert!(stdout.contains("Store this in your password manager before continuing."));
assert!(stdout.contains("Continue after storing it? [y/N]:"));
assert!(stderr.contains("Vault passphrase was not confirmed as stored"));
assert!(!vault_root.join("local-vault.lbox").exists());
}
#[test]
fn vault_lockbox_list_reports_owner_size_and_path() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-lockbox-list");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let lockbox = dir.join("listed.lbox");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
run_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "create", "--password"],
&vault_root,
&agent_root,
);
let table = run_output_without_content_key(
bin,
&["vault", "lockbox", "list"],
&vault_root,
&agent_root,
);
assert_success(&table);
let table = String::from_utf8_lossy(&table.stdout);
let header = table.lines().next().unwrap_or_default();
assert_contains_in_order(
header,
&["name", "state", "owner", "size", "lockbox_id", "path"],
);
let row = table.lines().nth(1).unwrap_or_default();
let columns = row.split_whitespace().collect::<Vec<_>>();
assert_eq!(columns[0], "listed.lbox");
assert_eq!(columns[1], "present");
assert_eq!(columns[2], "signed");
let size_digits = columns[3]
.chars()
.take_while(|character| character.is_ascii_digit() || *character == '.')
.count();
assert!(size_digits <= 6, "size was {}", columns[3]);
assert!(columns[3].ends_with('B'), "size was {}", columns[3]);
assert_eq!(
columns[5],
lockbox.canonicalize().unwrap().to_str().unwrap()
);
assert!(!table.contains("last_seen_unix_ms"));
let json = run_output_without_content_key(
bin,
&["vault", "lockbox", "list", "--format", "json"],
&vault_root,
&agent_root,
);
assert_success(&json);
let json = String::from_utf8_lossy(&json.stdout);
assert!(json.contains("\"name\":\"listed.lbox\""));
assert!(json.contains("\"owner\":\"signed\""));
assert!(json.contains("\"size\":\""));
assert!(json.contains("\"path\":\""));
let described = run_output_without_content_key(
bin,
&[
"vault",
"lockbox",
"list",
"--with-description",
"--format",
"json",
],
&vault_root,
&agent_root,
);
assert_success(&described);
assert!(String::from_utf8_lossy(&described.stdout).contains("\"description\""));
fs::remove_file(&lockbox).unwrap();
let tsv = run_output_without_content_key(
bin,
&["vault", "lockbox", "list", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&tsv);
let tsv = String::from_utf8_lossy(&tsv.stdout);
assert!(tsv.contains("listed.lbox\tmissing\t-\t-\t"));
}
#[test]
fn vault_lockbox_move_updates_file_sidecar_vault_and_default_session() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-lockbox-move");
let _ = fs::remove_dir_all(&dir);
let destination_dir = dir.join("archive");
fs::create_dir_all(&destination_dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let source = dir.join("source.lbox");
let destination = destination_dir.join("source.lbox");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
run_without_content_key(
bin,
&[source.to_str().unwrap(), "create", "--password"],
&vault_root,
&agent_root,
);
run_without_content_key(
bin,
&["session", "default", source.to_str().unwrap()],
&vault_root,
&agent_root,
);
fs::write(&destination, "occupied").unwrap();
let refused = run_output_without_content_key(
bin,
&[
"vault",
"lockbox",
"move",
source.to_str().unwrap(),
destination_dir.to_str().unwrap(),
],
&vault_root,
&agent_root,
);
assert!(!refused.status.success());
let stderr = String::from_utf8_lossy(&refused.stderr);
assert!(stderr.contains("destination already exists:"));
assert!(stderr.contains("no files or vault records were changed"));
assert!(source.exists());
fs::remove_file(&destination).unwrap();
let moved = run_output_without_content_key(
bin,
&[
"vault",
"lockbox",
"move",
source.to_str().unwrap(),
destination_dir.to_str().unwrap(),
],
&vault_root,
&agent_root,
);
assert_success(&moved);
assert!(!source.exists());
assert!(!dir.join(".source.lbox.lock").exists());
assert!(destination.exists());
assert!(destination_dir.join(".source.lbox.lock").exists());
let known = run_output_without_content_key(
bin,
&["vault", "lockbox", "list", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&known);
let known = String::from_utf8_lossy(&known.stdout);
assert!(known.contains(destination.canonicalize().unwrap().to_str().unwrap()));
assert!(!known.contains(source.to_str().unwrap()));
let session = run_output_without_content_key(bin, &["session"], &vault_root, &agent_root);
assert_success(&session);
assert!(String::from_utf8_lossy(&session.stdout)
.contains(destination.canonicalize().unwrap().to_str().unwrap()));
let listing = run_output_without_content_key(bin, &["list"], &vault_root, &agent_root);
assert_success(&listing);
}
#[test]
fn plain_filesystem_move_reports_stale_session_default_clearly() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("plain-lockbox-move");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let source = dir.join("source.lbox");
let destination = dir.join("moved.lbox");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
run_without_content_key(
bin,
&[source.to_str().unwrap(), "create", "--password"],
&vault_root,
&agent_root,
);
run_without_content_key(
bin,
&["session", "default", source.to_str().unwrap()],
&vault_root,
&agent_root,
);
fs::rename(&source, &destination).unwrap();
fs::rename(dir.join(".source.lbox.lock"), dir.join(".moved.lbox.lock")).unwrap();
let listing = run_output_without_content_key(bin, &["list"], &vault_root, &agent_root);
assert!(!listing.status.success());
let stderr = String::from_utf8_lossy(&listing.stderr);
assert!(stderr.contains("session default lockbox not found:"));
assert!(stderr.contains(source.file_name().unwrap().to_str().unwrap()));
let known = run_output_without_content_key(
bin,
&["vault", "lockbox", "list", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&known);
let known = String::from_utf8_lossy(&known.stdout);
assert!(known.contains("source.lbox\tmissing"));
}
#[test]
fn vault_init_generated_pass_phrase_accepts_stored_confirmation() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-pass-phrase-generated-yes");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let mut child = Command::new(bin)
.args(["vault", "init"])
.env("LOCKBOX_PASSWORD", "test-lockbox-password")
.env_remove("LOCKBOX_VAULT_PASSWORD")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child.stdin.as_mut().unwrap().write_all(b"\ny\n").unwrap();
let output = child.wait_with_output().unwrap();
assert_success(&output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Generated Vault passphrase:"));
assert!(stdout.contains("Continue after storing it? [y/N]:"));
assert!(stdout.contains("Vault created successfully."));
assert!(vault_root.join("local-vault.lbox").exists());
}
#[test]
fn interactive_create_and_profile_open_reuse_the_prompted_vault() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("interactive-create-open-vault");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let lockbox = dir.join("created.lbox");
let vault_password = "interactive-vault-password";
let mut init = Command::new(bin)
.args(["vault", "init"])
.env_remove("LOCKBOX_KEY")
.env_remove("LOCKBOX_PASSWORD")
.env_remove("LOCKBOX_VAULT_PASSWORD")
.env("LOCKBOX_PLATFORM_SECRET_STORE", "disabled")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
init.stdin
.as_mut()
.unwrap()
.write_all(format!("2\n{vault_password}\n{vault_password}\n").as_bytes())
.unwrap();
assert_success(&init.wait_with_output().unwrap());
let mut create = Command::new(bin)
.args([lockbox.to_str().unwrap(), "create"])
.env_remove("LOCKBOX_KEY")
.env_remove("LOCKBOX_PASSWORD")
.env_remove("LOCKBOX_VAULT_PASSWORD")
.env("LOCKBOX_PLATFORM_SECRET_STORE", "disabled")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
create
.stdin
.as_mut()
.unwrap()
.write_all(format!("{vault_password}\n").as_bytes())
.unwrap();
let output = create.wait_with_output().unwrap();
assert_success(&output);
assert!(lockbox.exists());
let mut open = Command::new(bin)
.args([lockbox.to_str().unwrap(), "open"])
.env_remove("LOCKBOX_KEY")
.env_remove("LOCKBOX_PASSWORD")
.env_remove("LOCKBOX_VAULT_PASSWORD")
.env("LOCKBOX_PLATFORM_SECRET_STORE", "disabled")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
open.stdin
.as_mut()
.unwrap()
.write_all(format!("{vault_password}\n").as_bytes())
.unwrap();
let output = open.wait_with_output().unwrap();
if is_session_agent_unavailable(&output) {
eprintln!("skipping Profile open assertions: Session Agent unavailable");
return;
}
assert_success(&output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Vault passphrase:"));
assert!(!stdout.contains("Password:"));
assert!(stdout.contains("Lockbox opened:"));
let mut reopen = Command::new(bin)
.args([lockbox.to_str().unwrap(), "open"])
.env_remove("LOCKBOX_KEY")
.env_remove("LOCKBOX_PASSWORD")
.env_remove("LOCKBOX_VAULT_PASSWORD")
.env("LOCKBOX_PLATFORM_SECRET_STORE", "disabled")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
reopen
.stdin
.as_mut()
.unwrap()
.write_all(format!("{vault_password}\n").as_bytes())
.unwrap();
let output = reopen.wait_with_output().unwrap();
assert_success(&output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Vault passphrase:"));
assert!(!stdout.contains("Password:"));
assert!(stdout.contains("Lockbox opened:"));
}
#[test]
fn interactive_password_open_distinguishes_vault_and_lockbox_passwords() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("interactive-password-open");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let lockbox = dir.join("password.lbox");
let vault_password = "interactive-vault-password";
let lockbox_password = "interactive-lockbox-password";
let init = Command::new(bin)
.args(["vault", "init"])
.env_remove("LOCKBOX_KEY")
.env("LOCKBOX_VAULT_PASSWORD", vault_password)
.env("LOCKBOX_PLATFORM_SECRET_STORE", "disabled")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.output()
.unwrap();
assert_success(&init);
let create = Command::new(bin)
.args([lockbox.to_str().unwrap(), "create", "--password"])
.env_remove("LOCKBOX_KEY")
.env("LOCKBOX_PASSWORD", lockbox_password)
.env("LOCKBOX_VAULT_PASSWORD", vault_password)
.env("LOCKBOX_PLATFORM_SECRET_STORE", "disabled")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.output()
.unwrap();
assert_success(&create);
let mut open = Command::new(bin)
.args([lockbox.to_str().unwrap(), "open"])
.env_remove("LOCKBOX_KEY")
.env_remove("LOCKBOX_PASSWORD")
.env_remove("LOCKBOX_VAULT_PASSWORD")
.env("LOCKBOX_PLATFORM_SECRET_STORE", "disabled")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
open.stdin
.as_mut()
.unwrap()
.write_all(format!("{vault_password}\n{lockbox_password}\n").as_bytes())
.unwrap();
let output = open.wait_with_output().unwrap();
if is_session_agent_unavailable(&output) {
eprintln!("skipping password open assertions: Session Agent unavailable");
return;
}
assert_success(&output);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Vault passphrase:"));
assert!(stdout.contains("Password:"));
assert!(stdout.contains("Lockbox opened:"));
}
#[test]
fn profile_only_open_never_falls_back_to_a_password_prompt() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("profile-open-no-password-fallback");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let lockbox = dir.join("profile-only.lbox");
let original_password = "original-vault-password";
let replacement_password = "replacement-vault-password";
let init = Command::new(bin)
.args(["vault", "init"])
.env_remove("LOCKBOX_KEY")
.env("LOCKBOX_VAULT_PASSWORD", original_password)
.env("LOCKBOX_PLATFORM_SECRET_STORE", "disabled")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.output()
.unwrap();
assert_success(&init);
let create = Command::new(bin)
.args([lockbox.to_str().unwrap(), "create"])
.env_remove("LOCKBOX_KEY")
.env("LOCKBOX_VAULT_PASSWORD", original_password)
.env("LOCKBOX_PLATFORM_SECRET_STORE", "disabled")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.output()
.unwrap();
assert_success(&create);
let replace = Command::new(bin)
.args(["vault", "init", "--overwrite"])
.env_remove("LOCKBOX_KEY")
.env("LOCKBOX_VAULT_PASSWORD", replacement_password)
.env("LOCKBOX_PLATFORM_SECRET_STORE", "disabled")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.output()
.unwrap();
assert_success(&replace);
let mut open = Command::new(bin)
.args([lockbox.to_str().unwrap(), "open"])
.env_remove("LOCKBOX_KEY")
.env_remove("LOCKBOX_PASSWORD")
.env_remove("LOCKBOX_VAULT_PASSWORD")
.env("LOCKBOX_PLATFORM_SECRET_STORE", "disabled")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
open.stdin
.as_mut()
.unwrap()
.write_all(format!("{replacement_password}\n").as_bytes())
.unwrap();
let output = open.wait_with_output().unwrap();
assert!(!output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stdout.contains("Vault passphrase:"));
assert!(!stdout.contains("Password:"));
assert!(stderr.contains("none of the local vault profiles can open"));
assert!(stderr.contains("lockbox has no password access"));
}
#[test]
fn open_requires_explicit_vault_init() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("open-vault-init");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let lockbox = dir.join("existing.lbox");
fs::write(&lockbox, b"not a lockbox").unwrap();
let open = run_output_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "open"],
&vault_root,
&agent_root,
);
assert!(!open.status.success());
assert!(String::from_utf8_lossy(&open.stderr)
.contains("local vault is not initialized; run `lockbox vault init` first"));
assert!(!vault_root.exists());
}
#[test]
fn vault_init_existing_is_noop_unless_verify_is_requested() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-init-existing");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
let existing =
run_output_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
assert_success(&existing);
let existing = String::from_utf8_lossy(&existing.stdout);
assert!(existing.contains("Local vault already exists."));
assert!(existing.contains("Path:"));
assert!(existing.contains("No changes made."));
assert!(existing.contains("Use `lockbox vault init --verify`"));
assert!(!existing.contains("Vault open successfully."));
assert!(!existing.contains("Directory:"));
let verified = run_output_without_content_key(
bin,
&["vault", "init", "--verify"],
&vault_root,
&agent_root,
);
assert_success(&verified);
let verified = String::from_utf8_lossy(&verified.stdout);
assert!(verified.contains("Local vault already exists."));
assert!(verified.contains("Vault opened successfully."));
assert!(!verified.contains("Directory:"));
}
#[test]
fn vault_init_verify_wrong_password_reports_vault_specific_error() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-init-wrong-password");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
let output = Command::new(bin)
.args(["vault", "init", "--verify"])
.env("LOCKBOX_PASSWORD", "test-lockbox-password")
.env("LOCKBOX_VAULT_PASSWORD", "wrong-vault-password")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("Vault open failed: check the Vault passphrase"));
assert!(stderr.contains("Vault file may be damaged"));
assert!(!stderr.contains("content key"));
assert!(!stderr.contains("contact keypair"));
assert!(!stderr.contains("local vault open state"));
let fingerprint = Command::new(bin)
.args(["vault", "profile", "fingerprint"])
.env("LOCKBOX_PASSWORD", "test-lockbox-password")
.env("LOCKBOX_VAULT_PASSWORD", "wrong-vault-password")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.output()
.unwrap();
assert!(!fingerprint.status.success());
let stderr = String::from_utf8_lossy(&fingerprint.stderr);
assert!(stderr.contains("Vault open failed: check the Vault passphrase"));
assert!(stderr.contains("Vault file may be damaged"));
assert!(!stderr.contains("corrupt lockbox header"));
}
#[test]
fn vault_init_overwrite_replaces_existing_vault_with_warning() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-init-overwrite");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
run_without_content_key(
bin,
&["vault", "profile", "create", "extra"],
&vault_root,
&agent_root,
);
let before = run_output_without_content_key(
bin,
&["vault", "profile", "list", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&before);
assert!(String::from_utf8_lossy(&before.stdout).contains("extra"));
let overwritten = run_output_without_content_key(
bin,
&["vault", "init", "--overwrite"],
&vault_root,
&agent_root,
);
assert_success(&overwritten);
let overwritten = String::from_utf8_lossy(&overwritten.stdout);
assert!(overwritten.contains(
"WARNING: replacing it will remove profiles, contacts and key-directory backups stored in this vault."
));
assert!(overwritten.contains("Vault replaced successfully."));
assert!(overwritten.contains("Created default profile: default"));
let after = run_output_without_content_key(
bin,
&["vault", "profile", "list", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&after);
let after = String::from_utf8_lossy(&after.stdout);
assert!(after.contains("default"));
assert!(!after.contains("extra"));
}
#[test]
fn vault_passphrase_changes_password_and_creates_backup() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-passphrase-change");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let new_password = "new-test-vault-password";
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
run_without_content_key(
bin,
&["vault", "profile", "create", "extra"],
&vault_root,
&agent_root,
);
let changed = Command::new(bin)
.args(["vault", "passphrase"])
.env("LOCKBOX_PASSWORD", "test-lockbox-password")
.env("LOCKBOX_VAULT_PASSWORD", "test-vault-password")
.env("LOCKBOX_NEW_VAULT_PASSWORD", new_password)
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.output()
.unwrap();
assert_success(&changed);
let changed = String::from_utf8_lossy(&changed.stdout);
assert!(changed.contains("Vault passphrase changed successfully."));
assert!(changed.contains("Backup:"));
assert!(fs::read_dir(&vault_root).unwrap().any(|entry| {
entry
.unwrap()
.file_name()
.to_string_lossy()
.starts_with("local-vault-before-passphrase-change-")
}));
let old_password = Command::new(bin)
.args(["vault", "init", "--verify"])
.env("LOCKBOX_PASSWORD", "test-lockbox-password")
.env("LOCKBOX_VAULT_PASSWORD", "test-vault-password")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.output()
.unwrap();
assert!(!old_password.status.success());
assert!(String::from_utf8_lossy(&old_password.stderr)
.contains("Vault open failed: check the Vault passphrase"));
let new_password_list = Command::new(bin)
.args(["vault", "profile", "list", "--format", "tsv"])
.env("LOCKBOX_PASSWORD", "test-lockbox-password")
.env("LOCKBOX_VAULT_PASSWORD", new_password)
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.output()
.unwrap();
assert_success(&new_password_list);
let new_password_list = String::from_utf8_lossy(&new_password_list.stdout);
assert!(new_password_list.contains("default"));
assert!(new_password_list.contains("extra"));
}
#[test]
fn vault_backup_and_restore_round_trip_encrypted_vault() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-backup-restore");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let backup = dir.join("vault.lockbox-backup");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
run_without_content_key(
bin,
&["vault", "profile", "create", "extra"],
&vault_root,
&agent_root,
);
let backed_up = run_output_without_content_key(
bin,
&["vault", "backup", backup.to_str().unwrap()],
&vault_root,
&agent_root,
);
assert_success(&backed_up);
let backed_up = String::from_utf8_lossy(&backed_up.stdout);
assert!(backed_up.contains("Backup completed successfully."));
assert!(backed_up.contains("Vault path: "));
assert!(backed_up.contains("local-vault.lbox"));
assert!(backed_up.contains("Backup path: "));
assert!(backup.exists());
let backup_path = backup.canonicalize().unwrap();
assert!(backed_up.contains(backup_path.to_str().unwrap()));
assert!(!backed_up.contains("backup="));
assert!(!backed_up.contains("vault_sha256="));
assert!(!backed_up.contains("created_at_utc="));
let overwritten_backup = run_output_without_content_key(
bin,
&["vault", "backup", backup.to_str().unwrap(), "--overwrite"],
&vault_root,
&agent_root,
);
assert_success(&overwritten_backup);
fs::remove_file(vault_root.join("local-vault.lbox")).unwrap();
let restored = run_output_without_content_key(
bin,
&["vault", "restore", backup.to_str().unwrap()],
&vault_root,
&agent_root,
);
assert_success(&restored);
assert!(String::from_utf8_lossy(&restored.stdout).contains("Vault restored successfully."));
let after_restore = run_output_without_content_key(
bin,
&["vault", "profile", "list", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&after_restore);
let after_restore = String::from_utf8_lossy(&after_restore.stdout);
assert!(after_restore.contains("default"));
assert!(after_restore.contains("extra"));
let refused = run_output_without_content_key(
bin,
&["vault", "restore", backup.to_str().unwrap()],
&vault_root,
&agent_root,
);
assert!(!refused.status.success());
assert!(String::from_utf8_lossy(&refused.stderr).contains("pass --overwrite"));
let overwritten = run_output_without_content_key(
bin,
&["vault", "restore", "--overwrite", backup.to_str().unwrap()],
&vault_root,
&agent_root,
);
assert_success(&overwritten);
let overwritten = String::from_utf8_lossy(&overwritten.stdout);
assert!(overwritten.contains("WARNING: replacing the local vault."));
assert!(overwritten.contains("Existing vault backed up before restore."));
assert!(overwritten.contains("Backup:\n "));
assert!(fs::read_dir(&vault_root).unwrap().any(|entry| {
entry
.unwrap()
.file_name()
.to_string_lossy()
.starts_with("local-vault-before-restore-")
}));
}
#[test]
fn vault_profile_create_names_default_and_rejects_public_key_output() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-profile-output");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let output = run_output_without_content_key(
bin,
&["vault", "profile", "create"],
&vault_root,
&agent_root,
);
assert_success(&output);
let output = String::from_utf8_lossy(&output.stdout);
assert!(output.contains("Using default profile name: default"));
assert!(output.contains("Created vault profile: default"));
assert!(output.contains("lockbox vault profile export <public-key-output> --name default"));
let named = run_output_without_content_key(
bin,
&["vault", "profile", "create", "named"],
&vault_root,
&agent_root,
);
assert_success(&named);
let named = String::from_utf8_lossy(&named.stdout);
assert!(named.contains("Created vault profile: named"));
assert!(named.contains("lockbox vault profile export <public-key-output> --name named"));
let refused_public_output = run_output_without_content_key(
bin,
&["vault", "profile", "create", "other", "other.pub"],
&vault_root,
&agent_root,
);
assert!(!refused_public_output.status.success());
assert!(String::from_utf8_lossy(&refused_public_output.stderr).contains("unexpected argument"));
}
#[test]
fn vault_profile_remove_requires_confirmation_and_force_bypasses_it() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-profile-remove-confirm");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
run_without_content_key(
bin,
&["vault", "profile", "create", "temp"],
&vault_root,
&agent_root,
);
let refused = run_output_without_content_key_with_stdin(
bin,
&["vault", "profile", "remove", "temp"],
&vault_root,
&agent_root,
"no\n",
);
assert_success(&refused);
assert!(String::from_utf8_lossy(&refused.stderr).contains("Remove vault profile 'temp'?"));
assert!(String::from_utf8_lossy(&refused.stdout).contains("Vault profile not removed: temp"));
let list = run_output_without_content_key(
bin,
&["vault", "profile", "ls", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&list);
assert!(String::from_utf8_lossy(&list.stdout).contains("temp"));
let forced = run_output_without_content_key(
bin,
&["vault", "profile", "remove", "--force", "temp"],
&vault_root,
&agent_root,
);
assert_success(&forced);
assert!(String::from_utf8_lossy(&forced.stdout).contains("Vault profile removed: temp"));
}
#[test]
fn vault_profile_rotate_history_and_access_refresh_flow() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = short_target_dir("profile-refresh");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let lockbox = dir.join("published.lbox");
let current_backup = dir.join("current.profile-backup");
let current_private = dir.join("current.private");
run_in(bin, &["vault", "init"], &vault_root, &agent_root);
run_in(
bin,
&[lockbox.to_str().unwrap(), "create"],
&vault_root,
&agent_root,
);
run_in(
bin,
&[lockbox.to_str().unwrap(), "access", "grant", "default"],
&vault_root,
&agent_root,
);
let rotated = run_output_in(
bin,
&["vault", "profile", "rotate", "default"],
&vault_root,
&agent_root,
);
assert_success(&rotated);
let rotated = String::from_utf8_lossy(&rotated.stdout);
assert!(rotated.contains("Rotated vault profile: default"));
assert!(rotated.contains("Active generation: 2"));
let history = run_output_in(
bin,
&["vault", "profile", "history", "default", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&history);
let history = String::from_utf8_lossy(&history.stdout);
assert!(history.contains("\t1\tretired\t"));
assert!(history.contains("\t2\tactive\t"));
run_in(
bin,
&[
"vault",
"profile",
"backup",
current_backup.to_str().unwrap(),
],
&vault_root,
&agent_root,
);
fs::write(
¤t_private,
extract_profile_private_key_backup(&fs::read_to_string(¤t_backup).unwrap()),
)
.unwrap();
let current_key = import_private_key_file(¤t_private).unwrap();
assert!(Lockbox::open(&lockbox, LockboxOpen::ContactKeyPair(current_key)).is_err());
let dry_run = run_output_in(
bin,
&[
lockbox.to_str().unwrap(),
"access",
"refresh",
"default",
"--dry-run",
],
&vault_root,
&agent_root,
);
assert_success(&dry_run);
let dry_run = String::from_utf8_lossy(&dry_run.stdout);
assert!(dry_run.contains("matching access entries: 1"));
assert!(dry_run.contains("No access entries were changed."));
let refreshed = run_output_in(
bin,
&[
lockbox.to_str().unwrap(),
"access",
"refresh",
"default",
"--yes",
],
&vault_root,
&agent_root,
);
assert_success(&refreshed);
assert!(String::from_utf8_lossy(&refreshed.stdout)
.contains("Refreshed access for 1 lockbox/profile pairs."));
let current_key = import_private_key_file(¤t_private).unwrap();
Lockbox::open(&lockbox, LockboxOpen::ContactKeyPair(current_key)).unwrap();
}
#[test]
fn vault_profile_export_reports_missing_output() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("vault-key-export-error");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
run_without_content_key(
bin,
&["vault", "profile", "create", "take-two"],
&vault_root,
&agent_root,
);
let output = run_output_without_content_key(
bin,
&["vault", "profile", "export", "--name", "take-two"],
&vault_root,
&agent_root,
);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr)
.contains("the following required arguments were not provided"));
}
#[test]
fn session_and_close_report_empty_cache_and_already_closed_state() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("open-list-close");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let lockbox = dir.join("state.lbox");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
run_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "create", "--password"],
&vault_root,
&agent_root,
);
let open_list = run_output_without_content_key(bin, &["session"], &vault_root, &agent_root);
assert_success(&open_list);
let open_list = String::from_utf8_lossy(&open_list.stdout);
assert!(open_list.contains("Default lockbox:"));
assert!(open_list.contains("Open lockboxes:"));
assert!(open_list.contains("none"));
run_without_content_key(
bin,
&["session", "default", lockbox.to_str().unwrap()],
&vault_root,
&agent_root,
);
let closed = run_output_without_content_key(bin, &["close"], &vault_root, &agent_root);
assert_success(&closed);
assert!(String::from_utf8_lossy(&closed.stdout).contains("already closed"));
let close_all =
run_output_without_content_key(bin, &["close", "--all"], &vault_root, &agent_root);
assert!(!close_all.status.success());
let closed = run_output_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "close"],
&vault_root,
&agent_root,
);
assert_success(&closed);
assert!(String::from_utf8_lossy(&closed.stdout).contains("already closed"));
let close_all =
run_output_without_content_key(bin, &["session", "close-all"], &vault_root, &agent_root);
assert_success(&close_all);
assert!(String::from_utf8_lossy(&close_all.stdout).contains("open Lockboxes closed"));
let refused_auto_open_disable = run_output_without_content_key_with_stdin(
bin,
&["session", "auto-open", "disable"],
&vault_root,
&agent_root,
"no\n",
);
assert_success(&refused_auto_open_disable);
assert!(
String::from_utf8_lossy(&refused_auto_open_disable.stderr).contains("Disable Auto Open?")
);
assert!(String::from_utf8_lossy(&refused_auto_open_disable.stdout)
.contains("Auto-open not disabled."));
let auto_open_disable = run_output_without_content_key(
bin,
&["session", "auto-open", "disable", "--yes"],
&vault_root,
&agent_root,
);
assert_success(&auto_open_disable);
let listing = run_output_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "list"],
&vault_root,
&agent_root,
);
assert!(!listing.status.success());
assert!(String::from_utf8_lossy(&listing.stderr).contains("Lockbox is closed"));
assert!(String::from_utf8_lossy(&listing.stderr).contains("Next step:"));
assert!(String::from_utf8_lossy(&listing.stderr).contains("lbx open"));
assert!(!String::from_utf8_lossy(&listing.stderr).contains("use the API intended"));
}
#[test]
fn session_default_sets_default_lockbox_for_commands() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = short_target_dir("active");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let lockbox = dir.join("active.lbox");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
run_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "create", "--password"],
&vault_root,
&agent_root,
);
let use_output = run_output_without_content_key(
bin,
&["session", "default", lockbox.to_str().unwrap()],
&vault_root,
&agent_root,
);
assert_success(&use_output);
assert!(agent_root.join("default-lockbox").is_file());
assert!(!vault_root.join(".default-lockbox").exists());
let session = run_output_without_content_key(bin, &["session"], &vault_root, &agent_root);
assert_success(&session);
let session = String::from_utf8_lossy(&session.stdout);
let default_lockbox = lockbox.canonicalize().unwrap();
assert!(session.contains("Default lockbox:"));
assert!(session.contains(default_lockbox.to_str().unwrap()));
let listing = run_output_without_content_key(bin, &["list"], &vault_root, &agent_root);
assert_success(&listing);
let missing_add = run_output_without_content_key(
bin,
&["add", dir.join("missing.md").to_str().unwrap()],
&vault_root,
&agent_root,
);
assert!(!missing_add.status.success());
assert!(String::from_utf8_lossy(&missing_add.stderr).contains("file not found:"));
assert!(!String::from_utf8_lossy(&missing_add.stderr).contains("unsupported host path"));
let source = dir.join("readme.md");
fs::write(&source, "default lockbox add\n").unwrap();
let add = run_output_without_content_key(
bin,
&["add", source.to_str().unwrap()],
&vault_root,
&agent_root,
);
assert_success(&add);
let listing = run_output_without_content_key(bin, &["list"], &vault_root, &agent_root);
assert_success(&listing);
assert!(String::from_utf8_lossy(&listing.stdout).contains("readme.md"));
let explicit = dir.join("explicit.lbox");
run_without_content_key(
bin,
&[explicit.to_str().unwrap(), "create", "--password"],
&vault_root,
&agent_root,
);
let explicit_source = dir.join("explicit.txt");
fs::write(&explicit_source, "explicit lockbox add\n").unwrap();
let explicit_add = run_output_without_content_key(
bin,
&[
explicit.to_str().unwrap(),
"add",
explicit_source.to_str().unwrap(),
],
&vault_root,
&agent_root,
);
assert_success(&explicit_add);
let explicit_listing = run_output_without_content_key(
bin,
&[explicit.to_str().unwrap(), "list"],
&vault_root,
&agent_root,
);
assert_success(&explicit_listing);
assert!(String::from_utf8_lossy(&explicit_listing.stdout).contains("explicit.txt"));
let assignment = run_output_without_content_key(
bin,
&["variables", "set", "AIR_WALLEX_API_KEY=XXX=YYY"],
&vault_root,
&agent_root,
);
assert_success(&assignment);
let assigned = run_output_without_content_key(
bin,
&["variable", "get", "AIR_WALLEX_API_KEY"],
&vault_root,
&agent_root,
);
assert_success(&assigned);
assert_eq!(String::from_utf8_lossy(&assigned.stdout), "XXX=YYY\n");
fs::remove_file(&lockbox).unwrap();
let missing_active = run_output_without_content_key(
bin,
&["add", source.to_str().unwrap(), "--to", "/after-delete.md"],
&vault_root,
&agent_root,
);
assert!(!missing_active.status.success());
let stderr = String::from_utf8_lossy(&missing_active.stderr);
assert!(stderr.contains(&format!(
"session default lockbox not found: {}",
default_lockbox.display()
)));
assert!(!stderr.contains("Check the supplied value"));
}
#[test]
fn session_default_lockbox_applies_to_lockbox_argument_variants() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = short_target_dir("active-variants");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let lockbox = dir.join("active.lbox");
run_in(bin, &["vault", "init"], &vault_root, &agent_root);
run_in(
bin,
&[lockbox.to_str().unwrap(), "create"],
&vault_root,
&agent_root,
);
run_in(
bin,
&["session", "default", lockbox.to_str().unwrap()],
&vault_root,
&agent_root,
);
let source = dir.join("a.txt");
fs::write(&source, "alpha").unwrap();
run_in(
bin,
&["add", source.to_str().unwrap(), "--to", "/docs/a.txt"],
&vault_root,
&agent_root,
);
let listing = run_output_in(
bin,
&["list", "--recursive", "--format", "tsv", "/docs"],
&vault_root,
&agent_root,
);
assert_success(&listing);
assert!(String::from_utf8_lossy(&listing.stdout).contains("/docs/a.txt"));
let cat = run_output_in(bin, &["cat", "/docs/a.txt"], &vault_root, &agent_root);
assert_success(&cat);
assert_eq!(String::from_utf8_lossy(&cat.stdout), "alpha");
let extracted = dir.join("extracted.txt");
let extracted_output = run_output_in(
bin,
&["extract", "/docs/a.txt", extracted.to_str().unwrap()],
&vault_root,
&agent_root,
);
assert_success(&extracted_output);
assert!(String::from_utf8_lossy(&extracted_output.stdout).contains("Extracted /docs/a.txt to"));
assert_eq!(fs::read_to_string(&extracted).unwrap(), "alpha");
let restored = dir.join("restore");
run_in(
bin,
&["extract", "--to", restored.to_str().unwrap()],
&vault_root,
&agent_root,
);
assert_eq!(
fs::read_to_string(restored.join("docs").join("a.txt")).unwrap(),
"alpha"
);
run_in(
bin,
&["rename", "/docs/a.txt", "/docs/b.txt"],
&vault_root,
&agent_root,
);
run_in(
bin,
&["rm", "--force", "/docs/b.txt"],
&vault_root,
&agent_root,
);
run_in(
bin,
&["var", "set", "/prod/API_KEY", "normal-key"],
&vault_root,
&agent_root,
);
let value = run_output_in(
bin,
&["variable", "get", "/prod/API_KEY"],
&vault_root,
&agent_root,
);
assert_success(&value);
assert_eq!(String::from_utf8_lossy(&value.stdout), "normal-key\n");
let variables = run_output_in(
bin,
&["variable", "list", "--format", "tsv", "/prod"],
&vault_root,
&agent_root,
);
assert_success(&variables);
assert!(String::from_utf8_lossy(&variables.stdout).contains("/prod/API_KEY"));
let exported = run_output_in(
bin,
&["variable", "export", "--format", "json", "/prod"],
&vault_root,
&agent_root,
);
assert_success(&exported);
assert!(String::from_utf8_lossy(&exported.stdout)
.contains("{\"name\":\"prod_API_KEY\",\"value\":\"normal-key\"}"));
run_in(
bin,
&["variable", "remove", "/prod/API_KEY"],
&vault_root,
&agent_root,
);
run_in(
bin,
&[
"form",
"define",
"login",
"--field",
"username:text",
"--field",
"password:secret",
],
&vault_root,
&agent_root,
);
let definitions = run_output_in(
bin,
&["form", "definitions", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&definitions);
assert!(String::from_utf8_lossy(&definitions.stdout).contains("login"));
run_in(
bin,
&[
"form",
"add",
"/work/github",
"--type",
"login",
"--set",
"username=bsutton",
],
&vault_root,
&agent_root,
);
run_in(
bin,
&["form", "set", "/work/github", "username", "alice"],
&vault_root,
&agent_root,
);
let username = run_output_in(
bin,
&["form", "get", "/work/github", "username"],
&vault_root,
&agent_root,
);
assert_success(&username);
assert_eq!(String::from_utf8_lossy(&username.stdout), "alice\n");
let shown = run_output_in(
bin,
&["form", "show", "/work/github"],
&vault_root,
&agent_root,
);
assert_success(&shown);
assert!(String::from_utf8_lossy(&shown.stdout).contains("field\tusername"));
let forms = run_output_in(
bin,
&["form", "list", "--format", "tsv", "/work"],
&vault_root,
&agent_root,
);
assert_success(&forms);
assert!(String::from_utf8_lossy(&forms.stdout).contains("/work/github"));
run_in(
bin,
&["form", "edit", "/work/github", "--set", "username=bob"],
&vault_root,
&agent_root,
);
run_in(
bin,
&["form", "remove", "/work/github"],
&vault_root,
&agent_root,
);
let visualize = run_output_in(bin, &["visualize"], &vault_root, &agent_root);
assert_success(&visualize);
assert!(String::from_utf8_lossy(&visualize.stdout).contains("Lockbox"));
let report = run_output_in(
bin,
&[
lockbox.to_str().unwrap(),
"doctor",
"recover",
"--dry-run",
"--format",
"json",
],
&vault_root,
&agent_root,
);
assert_success(&report);
assert!(String::from_utf8_lossy(&report.stdout).contains("file_count"));
run_in(
bin,
&["access", "grant", "default"],
&vault_root,
&agent_root,
);
let access = run_output_in(
bin,
&["access", "list", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&access);
let access = String::from_utf8_lossy(&access.stdout);
assert!(access.contains("\tdefault\tContact\t"));
let default_slot = access
.lines()
.find(|line| line.contains("\tdefault\tContact\t"))
.and_then(|line| line.split('\t').next())
.expect("default access slot");
let refresh = run_output_in(
bin,
&["access", "refresh", "default", "--dry-run"],
&vault_root,
&agent_root,
);
assert_success(&refresh);
assert!(String::from_utf8_lossy(&refresh.stdout).contains("matching access entries"));
let refresh_all = run_output_in(
bin,
&["access", "refresh", "--all", "--dry-run", "--yes"],
&vault_root,
&agent_root,
);
assert_success(&refresh_all);
let remove_access = run_output_in(
bin,
&["access", "revoke", default_slot],
&vault_root,
&agent_root,
);
assert!(!remove_access.status.success());
let remove_access = String::from_utf8_lossy(&remove_access.stderr);
assert!(remove_access.contains("cannot revoke the last access entry"));
assert!(!remove_access.contains("missing lockbox"));
run_in(
bin,
&["session", "default", "--clear"],
&vault_root,
&agent_root,
);
}
#[test]
fn auto_open_lockboxes_uses_remembered_password() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = short_target_dir("autolbx");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let lockbox = dir.join("remembered.lbox");
let init = run_output_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
if is_platform_secret_store_unavailable(&init) {
eprintln!("skipping Auto Open Lockbox assertions: platform credential store unavailable");
return;
}
assert_success(&init);
let vault_auto = run_output_without_content_key(
bin,
&["session", "auto-open", "vault"],
&vault_root,
&agent_root,
);
assert_success(&vault_auto);
assert!(String::from_utf8_lossy(&vault_auto.stdout).contains("vault"));
let auto = run_output_without_content_key(
bin,
&["session", "auto-open", "lockboxes"],
&vault_root,
&agent_root,
);
assert_success(&auto);
assert!(String::from_utf8_lossy(&auto.stdout).contains("lockboxes"));
run_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "create", "--password"],
&vault_root,
&agent_root,
);
let close = run_output_without_lockbox_password(
bin,
&[lockbox.to_str().unwrap(), "close"],
&vault_root,
&agent_root,
)
.output()
.unwrap();
if is_session_agent_unavailable(&close) {
eprintln!("skipping Auto Open Lockbox assertions: Session Agent unavailable");
return;
}
assert_success(&close);
let listing = run_output_without_lockbox_password(
bin,
&[lockbox.to_str().unwrap(), "list"],
&vault_root,
&agent_root,
)
.output()
.unwrap();
assert_success(&listing);
}
#[test]
fn auto_open_lockboxes_with_vault_profile_allows_first_add() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = short_target_dir("autoprofile");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let lockbox = dir.join("mystuff.lbox");
let source = dir.join("test.md");
fs::write(&source, "hello issue\n").unwrap();
let init = run_output_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
if is_platform_secret_store_unavailable(&init) {
eprintln!("skipping Auto Open Profile assertions: platform credential store unavailable");
return;
}
assert_success(&init);
run_without_content_key(
bin,
&["session", "auto-open", "lockboxes"],
&vault_root,
&agent_root,
);
run_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "create"],
&vault_root,
&agent_root,
);
run_without_content_key(
bin,
&["session", "default", lockbox.to_str().unwrap()],
&vault_root,
&agent_root,
);
let add = run_output_without_lockbox_password(
bin,
&["add", source.to_str().unwrap()],
&vault_root,
&agent_root,
)
.output()
.unwrap();
if is_session_agent_unavailable(&add) {
eprintln!("skipping Auto Open Profile assertions: Session Agent unavailable");
return;
}
assert_success(&add);
assert!(!String::from_utf8_lossy(&add.stderr).contains("contact-opened lockboxes"));
let listing = run_output_without_lockbox_password(
bin,
&[lockbox.to_str().unwrap(), "list"],
&vault_root,
&agent_root,
)
.output()
.unwrap();
assert_success(&listing);
assert!(String::from_utf8_lossy(&listing.stdout).contains("test.md"));
}
#[test]
fn open_accepts_password_sources_and_session_duration() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = short_target_dir("openpw");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
let lockbox = dir.join("session.lbox");
let lockbox_without_extension = dir.join("session");
run_without_content_key(bin, &["vault", "init"], &vault_root, &agent_root);
run_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "create", "--password"],
&vault_root,
&agent_root,
);
run_without_content_key(bin, &["session", "close-all"], &vault_root, &agent_root);
let env_open = run_output_without_lockbox_password(
bin,
&[
lockbox_without_extension.to_str().unwrap(),
"open",
"--password-env",
"LBX_TEST_PASSWORD",
],
&vault_root,
&agent_root,
)
.env("LBX_TEST_PASSWORD", "test-lockbox-password")
.output()
.unwrap();
if is_session_agent_unavailable(&env_open) {
eprintln!("skipping Session Agent assertions: Session Agent unavailable");
return;
}
assert_success(&env_open);
run_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "close"],
&vault_root,
&agent_root,
);
let password_file = dir.join("password.txt");
fs::write(&password_file, "test-lockbox-password\n").unwrap();
let file_open = run_output_without_lockbox_password(
bin,
&[
lockbox.to_str().unwrap(),
"open",
"--password-file",
password_file.to_str().unwrap(),
],
&vault_root,
&agent_root,
)
.output()
.unwrap();
assert_success(&file_open);
run_without_content_key(
bin,
&[lockbox.to_str().unwrap(), "close"],
&vault_root,
&agent_root,
);
run_without_content_key(
bin,
&["session", "auto-open", "disable", "--yes"],
&vault_root,
&agent_root,
);
let stdin_open = run_output_without_lockbox_password_with_stdin(
bin,
&[
lockbox.to_str().unwrap(),
"open",
"--password-stdin",
"--duration",
"2s",
],
&vault_root,
&agent_root,
"test-lockbox-password\n",
);
assert_success(&stdin_open);
let sessions = run_output_without_content_key(
bin,
&["session", "--format", "tsv"],
&vault_root,
&agent_root,
);
if is_session_agent_unavailable(&sessions) {
eprintln!("skipping Session Agent assertions: Session Agent unavailable");
return;
}
assert_success(&sessions);
assert!(
String::from_utf8_lossy(&sessions.stdout).contains(lockbox.to_str().unwrap()),
"session stdout:\n{}\nsession stderr:\n{}",
String::from_utf8_lossy(&sessions.stdout),
String::from_utf8_lossy(&sessions.stderr)
);
thread::sleep(Duration::from_secs(3));
let listing = Command::new(bin)
.args([lockbox.to_str().unwrap(), "list"])
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.output()
.unwrap();
assert!(!listing.status.success());
assert_eq!(listing.status.code(), Some(10));
assert!(String::from_utf8_lossy(&listing.stderr).contains("Lockbox is closed"));
}
fn is_session_agent_unavailable(output: &Output) -> bool {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
stdout.contains("Session Agent did not start")
|| stderr.contains("Session Agent did not start")
|| stdout.contains("Session Agent is not supported on this platform")
|| stderr.contains("Session Agent is not supported on this platform")
}
fn is_platform_secret_store_unavailable(output: &Output) -> bool {
String::from_utf8_lossy(&output.stderr).contains("platform credential store is unavailable")
}
#[test]
fn add_accepts_jobs_option_for_large_files() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("jobs-add");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("jobs.lbox");
let source = dir.join("large.bin");
let extracted = dir.join("extracted.bin");
let mut data = Vec::with_capacity(3 * 1024 * 1024);
for i in 0..(3 * 1024 * 1024) {
data.push((i % 251) as u8);
}
fs::write(&source, &data).unwrap();
run(
bin,
&[
lockbox.to_str().unwrap(),
"add",
"--jobs",
"2",
source.to_str().unwrap(),
"--to",
"/large.bin",
],
);
run(
bin,
&[
lockbox.to_str().unwrap(),
"extract",
"/large.bin",
extracted.to_str().unwrap(),
],
);
assert_eq!(fs::read(extracted).unwrap(), data);
let extracted_tree = dir.join("extracted-tree");
run(
bin,
&[
lockbox.to_str().unwrap(),
"extract",
"--to",
extracted_tree.to_str().unwrap(),
"--overwrite",
"--restore-permissions",
"--restore-symlinks",
],
);
assert_eq!(fs::read(extracted_tree.join("large.bin")).unwrap(), data);
}
#[test]
fn cli_secret_variables_require_explicit_source_and_redact_export() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("secret-variables");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("variables.lbox");
let secret_file = dir.join("secret.txt");
fs::write(&secret_file, "file-secret").unwrap();
let app_mode_set = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"APP_MODE",
"-v",
"prod",
],
);
assert_success(&app_mode_set);
assert!(String::from_utf8_lossy(&app_mode_set.stdout).contains("Variable set: /APP_MODE"));
run(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"EMPTY_VALUE",
"-v",
"",
],
);
let secret_set = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"-s",
"API_TOKEN",
"-f",
secret_file.to_str().unwrap(),
],
);
assert_success(&secret_set);
let secret_set = String::from_utf8_lossy(&secret_set.stdout);
assert!(secret_set.contains("Variable set: /API_TOKEN"));
assert!(!secret_set.contains("file-secret"));
let from_env = run_output_with_env(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"FROM_ENV",
"--from-env",
"E2E_VARIABLE_VALUE",
],
"E2E_VARIABLE_VALUE",
"environment-value",
);
assert_success(&from_env);
let from_stdin = run_output_with_stdin(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"FROM_STDIN",
"--stdin",
],
"stdin-value",
);
assert_success(&from_stdin);
let interactive = run_output_with_stdin(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"INTERACTIVE",
"--interactive",
],
"interactive-value\n",
);
assert_success(&interactive);
run(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"/production/APP_MODE",
"-v",
"prod-path",
],
);
run(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"/production/database/DATABASE_URL",
"-v",
"postgres://localhost/app",
],
);
run(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"/staging/APP_MODE",
"-v",
"staging-path",
],
);
let listing = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"list",
"--format",
"tsv",
],
);
assert_success(&listing);
let listing = String::from_utf8_lossy(&listing.stdout);
assert!(listing.contains("/APP_MODE"));
assert!(listing.contains("/EMPTY_VALUE"));
assert!(listing.contains("/API_TOKEN\tsecret"));
assert!(listing.contains("/production/APP_MODE"));
let all_listing = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"list",
"--all",
"--format",
"tsv",
],
);
assert_success(&all_listing);
assert!(String::from_utf8_lossy(&all_listing.stdout).contains("/FROM_ENV"));
let production_listing = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"list",
"--format",
"tsv",
"/production",
],
);
assert_success(&production_listing);
let production_listing = String::from_utf8_lossy(&production_listing.stdout);
assert!(production_listing.contains("/production/APP_MODE"));
assert!(production_listing.contains("/production/database/DATABASE_URL"));
assert!(!production_listing.contains("/staging/APP_MODE"));
let app_mode_listing = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"list",
"--format",
"tsv",
"**/APP_MODE",
],
);
assert_success(&app_mode_listing);
let app_mode_listing = String::from_utf8_lossy(&app_mode_listing.stdout);
assert!(app_mode_listing.contains("/APP_MODE"));
assert!(app_mode_listing.contains("/production/APP_MODE"));
assert!(app_mode_listing.contains("/staging/APP_MODE"));
assert!(!app_mode_listing.contains("/API_TOKEN"));
let export = run_output(bin, &[lockbox.to_str().unwrap(), "variable", "export"]);
assert_success(&export);
let export = String::from_utf8_lossy(&export.stdout);
assert!(export.contains("APP_MODE='prod'"));
assert!(export.contains("production_APP_MODE='prod-path'"));
assert!(export.contains("production_database_DATABASE_URL='postgres://localhost/app'"));
assert!(export.contains("staging_APP_MODE='staging-path'"));
assert!(!export.contains("API_TOKEN"));
assert!(!export.contains("file-secret"));
let production_export = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"export",
"/production",
],
);
assert_success(&production_export);
let production_export = String::from_utf8_lossy(&production_export.stdout);
assert!(production_export.contains("production_APP_MODE='prod-path'"));
assert!(
production_export.contains("production_database_DATABASE_URL='postgres://localhost/app'")
);
assert!(!production_export.contains("staging-path"));
let app_mode_export = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"export",
"**/APP_MODE",
],
);
assert_success(&app_mode_export);
let app_mode_export = String::from_utf8_lossy(&app_mode_export.stdout);
assert!(app_mode_export.contains("APP_MODE='prod'"));
assert!(app_mode_export.contains("production_APP_MODE='prod-path'"));
assert!(app_mode_export.contains("staging_APP_MODE='staging-path'"));
assert!(!app_mode_export.contains("DATABASE_URL"));
let powershell_export = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"export",
"--format",
"powershell",
],
);
assert_success(&powershell_export);
let powershell_export = String::from_utf8_lossy(&powershell_export.stdout);
assert!(powershell_export.contains("$env:APP_MODE = 'prod'"));
let cmd_export = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"export",
"--format",
"cmd",
],
);
assert_success(&cmd_export);
let cmd_export = String::from_utf8_lossy(&cmd_export.stdout);
assert!(cmd_export.contains("set \"APP_MODE=prod\""));
let json_export = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"export",
"--format",
"json",
],
);
assert_success(&json_export);
let json_export = String::from_utf8_lossy(&json_export.stdout);
assert!(json_export.contains("{\"name\":\"APP_MODE\",\"value\":\"prod\"}"));
assert!(json_export.contains("{\"name\":\"production_APP_MODE\",\"value\":\"prod-path\"}"));
let secret_get = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"get",
"-s",
"API_TOKEN",
],
);
assert_success(&secret_get);
assert_eq!(
String::from_utf8_lossy(&secret_get.stdout).trim(),
"file-secret"
);
let empty_get = run_output(
bin,
&[lockbox.to_str().unwrap(), "variable", "get", "EMPTY_VALUE"],
);
assert_success(&empty_get);
assert_eq!(String::from_utf8_lossy(&empty_get.stdout), "\n");
let missing_get = run_output(
bin,
&[lockbox.to_str().unwrap(), "variable", "get", "MISSING"],
);
assert!(!missing_get.status.success());
assert!(String::from_utf8_lossy(&missing_get.stderr).contains("not found"));
let missing_secret_get = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"get",
"--secret",
"MISSING",
],
);
assert!(!missing_secret_get.status.success());
assert!(String::from_utf8_lossy(&missing_secret_get.stderr).contains("not found"));
let report = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"doctor",
"recover",
"--dry-run",
"--format",
"tsv",
],
);
assert_success(&report);
let report = String::from_utf8_lossy(&report.stdout);
assert!(report.contains("variables_recovered\ttrue"));
assert!(report.contains("variable_count\t9"));
let token_output = dir.join("api-token.txt");
run(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"get",
"--secret",
"--output",
token_output.to_str().unwrap(),
"API_TOKEN",
],
);
assert_eq!(fs::read(&token_output).unwrap(), b"file-secret");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(
fs::metadata(&token_output).unwrap().permissions().mode() & 0o777,
0o600
);
}
let rejected_output = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"get",
"--secret",
"--output",
token_output.to_str().unwrap(),
"API_TOKEN",
],
);
assert!(!rejected_output.status.success());
assert_eq!(fs::read(&token_output).unwrap(), b"file-secret");
run(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"get",
"--secret",
"--output",
token_output.to_str().unwrap(),
"--overwrite",
"API_TOKEN",
],
);
assert_eq!(fs::read(&token_output).unwrap(), b"file-secret");
let rejected = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"API_TOKEN",
"positional-secret",
],
);
assert!(!rejected.status.success());
let rejected_value = run_output(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"-s",
"OTHER_TOKEN",
"--value",
"argument-secret",
],
);
assert!(!rejected_value.status.success());
}
#[test]
fn variable_set_secret_upgrades_a_normal_variable() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("secret-variable-upgrade");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("variables.lbox");
let secret_file = dir.join("secret.txt");
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
fs::write(&secret_file, "file-secret").unwrap();
run_in(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"UPGRADE_ME",
"-v",
"normal",
],
&vault_root,
&agent_root,
);
let upgraded = run_output_in(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"--secret",
"UPGRADE_ME",
"--file",
secret_file.to_str().unwrap(),
],
&vault_root,
&agent_root,
);
assert_success(&upgraded);
let value = run_output_in(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"get",
"--secret",
"UPGRADE_ME",
],
&vault_root,
&agent_root,
);
assert_success(&value);
assert_eq!(String::from_utf8_lossy(&value.stdout), "file-secret\n");
let listing = run_output_in(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"list",
"--format",
"tsv",
],
&vault_root,
&agent_root,
);
assert_success(&listing);
assert!(String::from_utf8_lossy(&listing.stdout).contains("/UPGRADE_ME\tsecret"));
let downgrade = run_output_in(
bin,
&[
lockbox.to_str().unwrap(),
"variable",
"set",
"UPGRADE_ME",
"-v",
"normal",
],
&vault_root,
&agent_root,
);
assert!(!downgrade.status.success());
}
#[test]
fn description_commands_support_every_value_source_and_clear() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("description-sources");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let lockbox = dir.join("description.lbox");
let description_file = dir.join("description.txt");
fs::write(&description_file, "from file").unwrap();
run(bin, &[lockbox.to_str().unwrap(), "create"]);
run(
bin,
&[
lockbox.to_str().unwrap(),
"description",
"set",
"positional description",
],
);
let get = run_output(bin, &[lockbox.to_str().unwrap(), "description", "get"]);
assert_success(&get);
assert_eq!(
String::from_utf8_lossy(&get.stdout),
"positional description\n"
);
run(
bin,
&[
lockbox.to_str().unwrap(),
"description",
"set",
"--file",
description_file.to_str().unwrap(),
],
);
let from_env = run_output_with_env(
bin,
&[
lockbox.to_str().unwrap(),
"description",
"set",
"--from-env",
"E2E_DESCRIPTION",
],
"E2E_DESCRIPTION",
"from environment",
);
assert_success(&from_env);
let from_stdin = run_output_with_stdin(
bin,
&[lockbox.to_str().unwrap(), "description", "set", "--stdin"],
"from stdin",
);
assert_success(&from_stdin);
let interactive = run_output_with_stdin(
bin,
&[
lockbox.to_str().unwrap(),
"description",
"set",
"--interactive",
],
"from prompt\n",
);
assert_success(&interactive);
let prompted = run_output(bin, &[lockbox.to_str().unwrap(), "description", "get"]);
assert_success(&prompted);
assert_eq!(String::from_utf8_lossy(&prompted.stdout), "from prompt\n");
run(bin, &[lockbox.to_str().unwrap(), "description", "clear"]);
let missing = run_output(bin, &[lockbox.to_str().unwrap(), "description", "get"]);
assert!(!missing.status.success());
assert!(String::from_utf8_lossy(&missing.stderr).contains("not found"));
}
#[test]
fn vault_profile_public_export_and_backup_restore_are_accepted_by_cli() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("key-formats");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let vault_root = dir.join("vault");
let agent_root = dir.join("agent");
run_in(
bin,
&["vault", "profile", "create", "default"],
&vault_root,
&agent_root,
);
let public_exports = [
("pem", None, "BEGIN LOCKBOX PUBLIC KEY"),
("jwk", Some("jwk"), "\"alg\": \"X25519-ML-KEM-768\""),
("jwks", Some("jwks"), "\"keys\""),
("raw", Some("raw-hex"), ""),
];
for (name, format, expected) in public_exports {
let path = dir.join(format!("public-{name}.key"));
let mut args = vec!["vault", "profile", "export"];
if let Some(format) = format {
args.extend(["--format", format]);
}
args.extend([path.to_str().unwrap(), "--name", "default"]);
let export = run_output_in(bin, &args, &vault_root, &agent_root);
assert_success(&export);
let export = String::from_utf8_lossy(&export.stdout);
assert!(export.contains("profile=default"));
assert!(export.contains("public_key_fingerprint="));
let text = String::from_utf8_lossy(&fs::read(&path).unwrap()).to_string();
if !expected.is_empty() {
assert!(text.contains(expected), "{name} public export: {text}");
}
let contact_key = import_public_key(&fs::read(&path).unwrap()).unwrap();
let contact_fingerprint = encode_hex(&public_key_fingerprint(&contact_key));
run_in(
bin,
&[
"vault",
"contact",
"import",
&format!("contact-{name}"),
path.to_str().unwrap(),
"--fingerprint",
&contact_fingerprint,
"--fingerprint-channel",
"phone-call-to-owner",
],
&vault_root,
&agent_root,
);
}
let backup = dir.join("default.profile-backup");
let backed_up = run_output_in(
bin,
&[
"vault",
"profile",
"backup",
backup.to_str().unwrap(),
"--name",
"default",
],
&vault_root,
&agent_root,
);
assert_success(&backed_up);
let backed_up = String::from_utf8_lossy(&backed_up.stdout);
assert!(backed_up.contains("Profile backup completed successfully."));
assert!(backed_up.contains("profile=default"));
let backup_text = fs::read_to_string(&backup).unwrap();
assert!(backup_text.contains("Profile backup:"));
assert!(backup_text.contains("Profile: default"));
assert!(backup_text.contains("BEGIN LOCKBOX PRIVATE KEY"));
assert!(backup_text.contains("Owner signing private key record (hex):"));
let overwritten_backup = run_output_in(
bin,
&[
"vault",
"profile",
"backup",
backup.to_str().unwrap(),
"--name",
"default",
"--overwrite",
],
&vault_root,
&agent_root,
);
assert_success(&overwritten_backup);
let shortened_boundary_backup = dir.join("shortened-boundary.profile-backup");
fs::write(
&shortened_boundary_backup,
backup_text
.replace(
"-----BEGIN LOCKBOX PRIVATE KEY-----",
"----BEGIN LOCKBOX PRIVATE KEY----",
)
.replace(
"-----END LOCKBOX PRIVATE KEY-----",
"----END LOCKBOX PRIVATE KEY----",
),
)
.unwrap();
let restored = run_output_in(
bin,
&[
"vault",
"profile",
"restore",
shortened_boundary_backup.to_str().unwrap(),
"--name",
"imported-backup",
],
&vault_root,
&agent_root,
);
assert_success(&restored);
let restored = String::from_utf8_lossy(&restored.stdout);
assert!(restored.contains("profile=imported-backup"));
assert!(restored.contains("owner_signing_key=restored"));
let missing_signing_backup = dir.join("missing-signing.profile-backup");
fs::write(
&missing_signing_backup,
backup_text
.split_once("Owner signing private key record (hex):")
.unwrap()
.0,
)
.unwrap();
let output = run_output_in(
bin,
&[
"vault",
"profile",
"restore",
missing_signing_backup.to_str().unwrap(),
"--name",
"missing-signing",
],
&vault_root,
&agent_root,
);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("missing owner signing private key"));
let invalid_backup = dir.join("invalid.profile-backup");
fs::write(&invalid_backup, "not a key").unwrap();
let output = run_output_in(
bin,
&[
"vault",
"profile",
"restore",
invalid_backup.to_str().unwrap(),
"--name",
"invalid",
],
&vault_root,
&agent_root,
);
assert!(!output.status.success());
let invalid_public = dir.join("invalid-public.key");
fs::write(&invalid_public, "not a public key").unwrap();
let output = run_output_in(
bin,
&[
"vault",
"contact",
"import",
"invalid",
invalid_public.to_str().unwrap(),
],
&vault_root,
&agent_root,
);
assert!(!output.status.success());
let profile_list = run_output_in(
bin,
&["vault", "profile", "list", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&profile_list);
let profile_list = String::from_utf8_lossy(&profile_list.stdout);
let contact_list = run_output_in(
bin,
&["vault", "contact", "list", "--format", "tsv"],
&vault_root,
&agent_root,
);
assert_success(&contact_list);
let contact_list = String::from_utf8_lossy(&contact_list.stdout);
for name in ["pem", "jwk", "jwks", "raw"] {
assert!(contact_list.contains(&format!("contact-{name}")));
}
assert!(profile_list.contains("imported-backup"));
}
#[test]
fn vault_profile_restore_replaces_default_profile_and_signing_key() {
let bin = env!("CARGO_BIN_EXE_lockbox");
let dir = unique_dir_named("profile-restore");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let source_vault_root = dir.join("source-vault");
let source_agent_root = dir.join("source-agent");
let target_vault_root = dir.join("target-vault");
let target_agent_root = dir.join("target-agent");
let backup_path = dir.join("default.profile-backup");
let source_init = run_output_without_content_key(
bin,
&["vault", "init"],
&source_vault_root,
&source_agent_root,
);
assert_success(&source_init);
let source_init = String::from_utf8_lossy(&source_init.stdout);
fs::write(&backup_path, source_init.as_bytes()).unwrap();
run_without_content_key(
bin,
&["vault", "init"],
&target_vault_root,
&target_agent_root,
);
let refused = run_output_without_content_key(
bin,
&["vault", "profile", "restore", backup_path.to_str().unwrap()],
&target_vault_root,
&target_agent_root,
);
assert!(!refused.status.success());
assert!(String::from_utf8_lossy(&refused.stderr).contains("vault profile default"));
let restored = run_output_without_content_key(
bin,
&[
"vault",
"profile",
"restore",
backup_path.to_str().unwrap(),
"--overwrite",
],
&target_vault_root,
&target_agent_root,
);
assert_success(&restored);
let restored = String::from_utf8_lossy(&restored.stdout);
assert!(restored.contains("WARNING: replacing vault profile: default"));
assert!(restored.contains("Existing vault backed up before profile restore."));
assert!(restored.contains("Backup:\n "));
assert!(restored.contains("profile=default"));
assert!(restored.contains("public_key_fingerprint="));
assert!(restored.contains("owner_signing_key=restored"));
assert!(fs::read_dir(&target_vault_root).unwrap().any(|entry| {
entry
.unwrap()
.file_name()
.to_string_lossy()
.starts_with("local-vault-before-profile-restore-default-")
}));
let vault_password = SecretString::try_from_bytes(b"test-vault-password".to_vec()).unwrap();
let source_vault = VaultDirectory::open_or_create(&source_vault_root, &vault_password).unwrap();
let target_vault = VaultDirectory::open_or_create(&target_vault_root, &vault_password).unwrap();
let source_profile = source_vault
.load_private_key(VaultDirectory::DEFAULT_KEY_NAME)
.unwrap();
let target_profile = target_vault
.load_private_key(VaultDirectory::DEFAULT_KEY_NAME)
.unwrap();
assert_eq!(source_profile.public_key(), target_profile.public_key());
assert_eq!(
source_profile.private_key_record().unwrap(),
target_profile.private_key_record().unwrap()
);
let source_signing = source_vault
.load_owner_signing_key(VaultDirectory::DEFAULT_KEY_NAME)
.unwrap();
let target_signing = target_vault
.load_owner_signing_key(VaultDirectory::DEFAULT_KEY_NAME)
.unwrap();
assert_eq!(
source_signing.private_key_record().unwrap(),
target_signing.private_key_record().unwrap()
);
}
fn run(bin: &str, args: &[&str]) {
let output = run_output(bin, args);
assert_success(&output);
}
fn run_in(bin: &str, args: &[&str], vault_root: &PathBuf, agent_root: &PathBuf) {
let output = run_output_in(bin, args, vault_root, agent_root);
assert_success(&output);
}
fn run_without_content_key(bin: &str, args: &[&str], vault_root: &PathBuf, agent_root: &PathBuf) {
let output = run_output_without_content_key(bin, args, vault_root, agent_root);
assert_success(&output);
}
fn run_output(bin: &str, args: &[&str]) -> Output {
run_output_in(
bin,
args,
&unique_dir().join("vault"),
&unique_dir().join("agent"),
)
}
fn run_output_with_env(bin: &str, args: &[&str], name: &str, value: &str) -> Output {
let vault_root = unique_dir().join("vault");
let agent_root = unique_dir().join("agent");
Command::new(bin)
.args(args)
.env("LOCKBOX_KEY", "test-key")
.env("LOCKBOX_VAULT_PASSWORD", "test-vault-password")
.env("LOCKBOX_SESSION_AGENT_DIR", &agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(&agent_root))
.env("LOCKBOX_VAULT_DIR", &vault_root)
.env(name, value)
.output()
.unwrap()
}
fn run_output_in(bin: &str, args: &[&str], vault_root: &PathBuf, agent_root: &PathBuf) -> Output {
Command::new(bin)
.args(args)
.env("LOCKBOX_KEY", "test-key")
.env("LOCKBOX_VAULT_PASSWORD", "test-vault-password")
.env("LOCKBOX_SESSION_AGENT_DIR", agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(agent_root))
.env("LOCKBOX_VAULT_DIR", vault_root)
.output()
.unwrap()
}
fn run_output_with_stdin(bin: &str, args: &[&str], stdin: &str) -> Output {
run_output_in_with_stdin(
bin,
args,
&unique_dir().join("vault"),
&unique_dir().join("agent"),
stdin,
)
}
fn run_output_in_with_stdin(
bin: &str,
args: &[&str],
vault_root: &PathBuf,
agent_root: &PathBuf,
stdin: &str,
) -> Output {
let mut child = Command::new(bin)
.args(args)
.env("LOCKBOX_KEY", "test-key")
.env("LOCKBOX_VAULT_PASSWORD", "test-vault-password")
.env("LOCKBOX_SESSION_AGENT_DIR", agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(agent_root))
.env("LOCKBOX_VAULT_DIR", vault_root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.as_mut()
.unwrap()
.write_all(stdin.as_bytes())
.unwrap();
child.wait_with_output().unwrap()
}
fn run_output_without_content_key(
bin: &str,
args: &[&str],
vault_root: &PathBuf,
agent_root: &PathBuf,
) -> Output {
Command::new(bin)
.args(args)
.env("LOCKBOX_PASSWORD", "test-lockbox-password")
.env("LOCKBOX_VAULT_PASSWORD", "test-vault-password")
.env("LOCKBOX_SESSION_AGENT_DIR", agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(agent_root))
.env("LOCKBOX_VAULT_DIR", vault_root)
.output()
.unwrap()
}
fn run_output_without_lockbox_password(
bin: &str,
args: &[&str],
vault_root: &PathBuf,
agent_root: &PathBuf,
) -> Command {
let mut command = Command::new(bin);
command
.args(args)
.env("LOCKBOX_VAULT_PASSWORD", "test-vault-password")
.env("LOCKBOX_SESSION_AGENT_DIR", agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(agent_root))
.env("LOCKBOX_VAULT_DIR", vault_root);
command
}
fn run_output_without_lockbox_password_with_stdin(
bin: &str,
args: &[&str],
vault_root: &PathBuf,
agent_root: &PathBuf,
stdin: &str,
) -> Output {
let mut child = run_output_without_lockbox_password(bin, args, vault_root, agent_root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.as_mut()
.unwrap()
.write_all(stdin.as_bytes())
.unwrap();
child.wait_with_output().unwrap()
}
fn run_output_without_content_key_with_stdin(
bin: &str,
args: &[&str],
vault_root: &PathBuf,
agent_root: &PathBuf,
stdin: &str,
) -> Output {
let mut child = Command::new(bin)
.args(args)
.env("LOCKBOX_PASSWORD", "test-lockbox-password")
.env("LOCKBOX_VAULT_PASSWORD", "test-vault-password")
.env("LOCKBOX_SESSION_AGENT_DIR", agent_root)
.env("LOCKBOX_SESSION_AGENT_LOG", agent_log_path(agent_root))
.env("LOCKBOX_VAULT_DIR", vault_root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.as_mut()
.unwrap()
.write_all(stdin.as_bytes())
.unwrap();
child.wait_with_output().unwrap()
}
fn agent_log_path(agent_root: &Path) -> PathBuf {
agent_root.join("agent.log")
}
fn assert_success(output: &Output) {
assert!(
output.status.success(),
"command failed\nstatus: {}\nstdout:\n{}\nstderr:\n{}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn assert_contains_in_order(text: &str, needles: &[&str]) {
let mut start = 0;
for needle in needles {
let Some(index) = text[start..].find(needle) else {
panic!("missing {needle:?} after byte {start} in:\n{text}");
};
start += index + needle.len();
}
}
fn extract_profile_private_key_backup(text: &str) -> String {
let begin = "-----BEGIN LOCKBOX PRIVATE KEY-----";
let end = "-----END LOCKBOX PRIVATE KEY-----";
let start = text.find(begin).expect("profile private key begin block");
let end = text[start..]
.find(end)
.map(|index| start + index + end.len())
.expect("profile private key end block");
let mut block = text[start..end].to_string();
block.push('\n');
block
}
fn extract_owner_signing_private_key_hex(text: &str) -> String {
let (_, rest) = text
.split_once("Owner signing private key record (hex):")
.expect("owner signing private key block");
rest.lines()
.map(str::trim)
.skip_while(|line| line.is_empty())
.take_while(|line| !line.is_empty() && line.bytes().all(|byte| byte.is_ascii_hexdigit()))
.collect::<String>()
}
fn unique_dir() -> PathBuf {
unique_thread_dir_path("lockbox-cli-flow")
}
fn unique_dir_named(label: &str) -> PathBuf {
unique_dir_path("lockbox", label)
}
fn short_target_dir(label: &str) -> PathBuf {
short_dir_path(label)
}