second-brain-sync 0.4.0

Bidirectional sync for second-brain: SSH transport, JSONL change log, conflict resolution
Documentation
use std::ffi::OsString;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;

use second_brain_sync::ssh;

fn osstr(s: &str) -> OsString {
    OsString::from(s)
}

#[test]
fn ssh_args_use_strict_checking_and_pinned_known_hosts() {
    let known_hosts = PathBuf::from("/tmp/test_known_hosts");
    let args = ssh::ssh_args(&known_hosts);

    let mut pairs: Vec<(OsString, OsString)> = Vec::new();
    let mut iter = args.into_iter();
    while let Some(flag) = iter.next() {
        assert_eq!(flag, osstr("-o"), "every option entry must be -o prefixed");
        let value = iter.next().expect("-o without value");
        let value_str = value.to_string_lossy().to_string();
        let (k, v) = value_str.split_once('=').expect("option must be KEY=VALUE");
        pairs.push((OsString::from(k), OsString::from(v)));
    }

    let by_key: std::collections::HashMap<_, _> = pairs.into_iter().collect();

    assert_eq!(
        by_key.get(&osstr("StrictHostKeyChecking")),
        Some(&osstr("yes")),
        "StrictHostKeyChecking must be yes so a missing/mismatched host key blocks the connection"
    );
    assert_eq!(
        by_key.get(&osstr("UserKnownHostsFile")),
        Some(&osstr("/tmp/test_known_hosts")),
        "UserKnownHostsFile must point at the sync-owned file"
    );
    assert_eq!(
        by_key.get(&osstr("GlobalKnownHostsFile")),
        Some(&osstr("/dev/null")),
        "GlobalKnownHostsFile must be /dev/null so /etc/ssh/ssh_known_hosts cannot grant trust"
    );
    assert!(
        by_key.contains_key(&osstr("ServerAliveInterval")),
        "ServerAliveInterval keeps long sync streams alive"
    );
}

#[test]
fn known_hosts_path_in_config_dir_is_named_known_hosts() {
    let config_dir = PathBuf::from("/home/example/.second-brain");
    let path = ssh::known_hosts_path_in(&config_dir);
    assert_eq!(
        path,
        PathBuf::from("/home/example/.second-brain/known_hosts")
    );
}

#[test]
fn parse_keyscan_output_strips_comments_and_blanks() {
    let raw = "# pi.local SSH-2.0-OpenSSH_8.4\n\
               pi.local ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleEdKey\n\
               \n\
               # pi.local SSH-2.0-OpenSSH_8.4\n\
               pi.local ssh-rsa AAAAB3NzaC1yc2EAAAExampleRsaKey\n\
               # trailing comment\n";

    let lines = ssh::parse_keyscan_output(raw);
    assert_eq!(lines.len(), 2, "exactly two key lines expected");

    assert_eq!(lines[0].host, "pi.local");
    assert_eq!(lines[0].key_type, "ssh-ed25519");
    assert_eq!(lines[0].public_key, "AAAAC3NzaC1lZDI1NTE5AAAAIExampleEdKey");

    assert_eq!(lines[1].host, "pi.local");
    assert_eq!(lines[1].key_type, "ssh-rsa");
}

#[test]
fn parse_keyscan_output_rejects_malformed_lines() {
    let raw = "pi.local ssh-ed25519\n\
               pi.local\n\
               pi.local ssh-rsa AAAA validkeyline\n";
    let lines = ssh::parse_keyscan_output(raw);
    assert_eq!(
        lines.len(),
        1,
        "lines lacking host/type/key must be discarded"
    );
}

#[test]
fn compose_known_host_entry_emits_canonical_line() {
    let line = ssh::KeyscanLine {
        host: "pi.local".to_string(),
        key_type: "ssh-ed25519".to_string(),
        public_key: "AAAAC3NzaC1lZDI1NTE5AAAAIExampleEdKey".to_string(),
    };
    let entry = ssh::compose_known_host_entry(&line);
    assert_eq!(
        entry,
        "pi.local ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleEdKey\n"
    );
}

fn ed_line(host: &str) -> ssh::KeyscanLine {
    ssh::KeyscanLine {
        host: host.to_string(),
        key_type: "ssh-ed25519".to_string(),
        public_key: "AAAAC3NzaC1lZDI1NTE5AAAAIExampleEdKey".to_string(),
    }
}

#[test]
fn append_known_host_creates_file_with_0600_mode() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("known_hosts");
    ssh::append_known_host(&path, &[ed_line("pi.local")]).unwrap();

    let meta = std::fs::metadata(&path).unwrap();
    let mode = meta.permissions().mode() & 0o777;
    assert_eq!(
        mode, 0o600,
        "known_hosts must be 0600; secrets-equivalent file"
    );

    let body = std::fs::read_to_string(&path).unwrap();
    assert_eq!(
        body,
        "pi.local ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleEdKey\n"
    );
}

#[test]
fn append_known_host_tightens_permissions_on_existing_file() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("known_hosts");
    std::fs::write(&path, "").unwrap();
    std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();

    ssh::append_known_host(&path, &[ed_line("pi.local")]).unwrap();

    let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
    assert_eq!(
        mode, 0o600,
        "permissive existing file must be tightened on append"
    );
}

#[test]
fn append_known_host_is_idempotent() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("known_hosts");
    let line = ed_line("pi.local");

    ssh::append_known_host(&path, &[line.clone()]).unwrap();
    ssh::append_known_host(&path, &[line.clone()]).unwrap();

    let body = std::fs::read_to_string(&path).unwrap();
    let occurrences = body.matches("ssh-ed25519").count();
    assert_eq!(
        occurrences, 1,
        "appending an identical entry twice must not duplicate it"
    );
}

#[test]
fn untrusted_host_hint_names_the_remediation_command() {
    let hint = ssh::untrusted_host_hint("pi.local");
    assert!(
        hint.contains("sb sync trust pi.local"),
        "hint must spell out the exact recovery command, got: {hint}"
    );
    assert!(
        hint.contains("known_hosts"),
        "hint should reference known_hosts so user knows where trust is stored"
    );
}

#[test]
fn append_known_host_keeps_distinct_keys_for_same_host() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("known_hosts");

    let ed = ed_line("pi.local");
    let rsa = ssh::KeyscanLine {
        host: "pi.local".to_string(),
        key_type: "ssh-rsa".to_string(),
        public_key: "AAAAB3NzaC1yc2EAAAExampleRsaKey".to_string(),
    };

    ssh::append_known_host(&path, &[ed, rsa]).unwrap();

    let body = std::fs::read_to_string(&path).unwrap();
    assert!(body.contains("ssh-ed25519"));
    assert!(body.contains("ssh-rsa"));
}