note-to-self-cli 0.1.0

Encrypted local-first journaling CLI with sync and locked journals.
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::process::{Command, Output};

fn note(home: &tempfile::TempDir) -> Command {
    let mut command = Command::new(env!("CARGO_BIN_EXE_note"));
    command
        .env("NOTE_TO_SELF_HOME", home.path())
        .env_remove("NOTE_TO_SELF_PASSWORD")
        .env_remove("JRNL2_PASSWORD")
        .env_remove("NOTE_TO_SELF_JOURNAL_PASSWORD")
        .env_remove("NOTE_TO_SELF_LOCK_PASSWORD");
    command
}

fn assert_success(output: Output) -> String {
    if !output.status.success() {
        panic!(
            "command failed\nstatus: {}\nstdout:\n{}\nstderr:\n{}",
            output.status,
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr),
        );
    }
    String::from_utf8(output.stdout).expect("stdout should be utf-8")
}

fn empty_editor(home: &tempfile::TempDir) -> std::path::PathBuf {
    let path = home.path().join("empty-editor.sh");
    fs::write(&path, "#!/usr/bin/env sh\nexit 0\n").unwrap();
    let mut permissions = fs::metadata(&path).unwrap().permissions();
    permissions.set_mode(0o755);
    fs::set_permissions(&path, permissions).unwrap();
    path
}

fn sample_png(home: &tempfile::TempDir) -> std::path::PathBuf {
    let path = home.path().join("pixel.png");
    fs::write(
        &path,
        [
            0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48,
            0x44, 0x52,
        ],
    )
    .unwrap();
    path
}

#[test]
fn journal_default_without_name_prints_current_default() {
    let home = tempfile::tempdir().unwrap();

    assert_success(
        note(&home)
            .args([
                "init",
                "--local",
                "--username",
                "alice",
                "--journal",
                "work",
            ])
            .output()
            .unwrap(),
    );

    let stdout = assert_success(note(&home).args(["journal", "default"]).output().unwrap());
    assert_eq!(stdout.trim(), "work");
}

#[test]
fn empty_editor_for_default_new_note_exits_without_error() {
    let home = tempfile::tempdir().unwrap();

    assert_success(
        note(&home)
            .args([
                "init",
                "--local",
                "--username",
                "alice",
                "--journal",
                "personal",
            ])
            .output()
            .unwrap(),
    );

    let editor = empty_editor(&home);
    let mut create = note(&home);
    create
        .env("EDITOR", editor)
        .env("NOTE_TO_SELF_PASSWORD", "correct horse");
    let stdout = assert_success(create.output().unwrap());
    assert_eq!(stdout.trim(), "");

    let stdout = assert_success(note(&home).args(["list"]).output().unwrap());
    assert_eq!(stdout.trim(), "");
}

#[test]
fn account_password_is_cached_after_first_use() {
    let home = tempfile::tempdir().unwrap();

    assert_success(
        note(&home)
            .args([
                "init",
                "--local",
                "--username",
                "alice",
                "--journal",
                "personal",
            ])
            .output()
            .unwrap(),
    );

    let mut create = note(&home);
    create.env("NOTE_TO_SELF_PASSWORD", "correct horse");
    assert_success(create.args(["cached account key entry"]).output().unwrap());

    let stdout = assert_success(note(&home).args(["list"]).output().unwrap());
    assert!(stdout.contains("cached account key entry"));
}

#[test]
fn image_upload_creates_entry_with_encrypted_attachment() {
    let home = tempfile::tempdir().unwrap();
    let image = sample_png(&home);

    assert_success(
        note(&home)
            .args([
                "init",
                "--local",
                "--username",
                "alice",
                "--journal",
                "personal",
            ])
            .output()
            .unwrap(),
    );

    let mut create = note(&home);
    create.env("NOTE_TO_SELF_PASSWORD", "correct horse");
    let stdout = assert_success(
        create
            .args(["--img", image.to_str().unwrap(), "trip", "photo"])
            .output()
            .unwrap(),
    );
    assert!(stdout.contains("created"));
    assert!(stdout.contains("[image: pixel.png"));

    let stdout = assert_success(note(&home).args(["list"]).output().unwrap());
    assert!(stdout.contains("trip photo"));
    assert!(stdout.contains("[image: pixel.png"));

    let export = assert_success(note(&home).args(["export"]).output().unwrap());
    assert!(export.contains("\"attachments\""));
    assert!(export.contains("\"file_name\": \"pixel.png\""));
    assert!(export.contains("note-image:"));
}

#[test]
fn todo_text_creates_todo_with_metadata_without_subcommands() {
    let home = tempfile::tempdir().unwrap();

    assert_success(
        note(&home)
            .args([
                "init",
                "--local",
                "--username",
                "alice",
                "--journal",
                "personal",
            ])
            .output()
            .unwrap(),
    );

    let mut create = note(&home);
    create.env("NOTE_TO_SELF_PASSWORD", "correct horse");
    let stdout = assert_success(
        create
            .args([
                "todo",
                "--priority",
                "high",
                "--due",
                "2026-06-01",
                "renew",
                "passport",
                "#errand",
            ])
            .output()
            .unwrap(),
    );
    assert!(stdout.contains("created todo"));

    let stdout = assert_success(note(&home).args(["search", "passport"]).output().unwrap());
    assert!(stdout.contains("[ ]"));
    assert!(stdout.contains("p:high"));
    assert!(stdout.contains("due:2026-06-01"));
    assert!(stdout.contains("renew passport"));
    assert!(stdout.contains("#errand"));
}