hashtree-cli 0.2.53

Hashtree daemon and CLI - content-addressed storage with P2P sync
Documentation
mod common;

use common::htree_bin;
use nostr::nips::nip19::ToBech32;
use nostr::Keys;
use std::process::Command;
use tempfile::TempDir;

fn run_user_command(config_dir: &std::path::Path, home_dir: &std::path::Path) -> (String, String) {
    let output = Command::new(htree_bin())
        .arg("user")
        .env("HOME", home_dir)
        .env("HTREE_CONFIG_DIR", config_dir)
        .output()
        .expect("run htree user");

    assert!(
        output.status.success(),
        "htree user failed.\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );

    (
        String::from_utf8(output.stdout).expect("stdout utf-8"),
        String::from_utf8(output.stderr).expect("stderr utf-8"),
    )
}

#[test]
fn user_lists_default_signer_first_and_aliases_separately() {
    let tmp = TempDir::new().expect("temp dir");
    let config_dir = tmp.path().join("config");
    std::fs::create_dir_all(&config_dir).expect("create config dir");

    let work = Keys::generate();
    let default = Keys::generate();
    let alias = Keys::generate();

    let work_nsec = work.secret_key().to_bech32().expect("work nsec");
    let default_nsec = default.secret_key().to_bech32().expect("default nsec");
    let work_npub = work.public_key().to_bech32().expect("work npub");
    let default_npub = default.public_key().to_bech32().expect("default npub");
    let alias_npub = alias.public_key().to_bech32().expect("alias npub");

    std::fs::write(
        config_dir.join("keys"),
        format!("{work_nsec} work\n{default_nsec} default\n"),
    )
    .expect("write keys");
    std::fs::write(
        config_dir.join("aliases"),
        format!("{alias_npub} coworker\n"),
    )
    .expect("write aliases");

    let (stdout, stderr) = run_user_command(&config_dir, tmp.path());
    let lines: Vec<&str> = stdout.lines().collect();

    assert_eq!(lines[0], format!("{default_npub} (default)"));
    assert_eq!(lines[1], format!("{work_npub} (work)"));
    assert_eq!(lines[2], "");
    assert_eq!(lines[3], "Aliases:");
    assert_eq!(lines[4], format!("{alias_npub} (coworker)"));
    assert!(
        !stdout.contains("nsec1"),
        "stdout should not include secret keys:\n{stdout}"
    );
    assert!(
        !stderr.contains("Generated new identity"),
        "existing signing keys should not trigger generation:\n{stderr}"
    );
}

#[test]
fn user_generates_and_shows_a_signing_identity_when_missing() {
    let tmp = TempDir::new().expect("temp dir");
    let config_dir = tmp.path().join("config");
    std::fs::create_dir_all(&config_dir).expect("create config dir");

    let (stdout, stderr) = run_user_command(&config_dir, tmp.path());
    let first_line = stdout.lines().next().expect("npub output");

    assert!(
        first_line.starts_with("npub1"),
        "expected npub on first line, got:\n{stdout}"
    );
    assert!(
        first_line.contains("(self)"),
        "generated key should retain the default self alias:\n{stdout}"
    );
    assert!(
        !stdout.contains("nsec1"),
        "stdout should not include secret keys:\n{stdout}"
    );
    assert!(
        stderr.contains("Generated new identity"),
        "generation should still be reported on stderr:\n{stderr}"
    );
}