wsb 0.275.7622

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! The file a command line and its daemon meet at.

use super::*;

fn rendezvous(root: &Path) -> Rendezvous {
    Rendezvous {
        version: "1.2.3".to_string(),
        pid: 4242,
        http_address: std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
        http_port: 3000,
        worktree_root: root.to_path_buf(),
        started_at: 1_700_000_000,
    }
}

/// TEST9850: what the daemon writes is what the command line reads.
#[test]
fn test9850_a_rendezvous_survives_the_round_trip() {
    let dir = tempfile::tempdir().expect("a scratch directory");
    let written = rendezvous(dir.path());
    written.write(dir.path()).expect("the file is written");

    let read = Rendezvous::read(dir.path())
        .expect("the file is readable")
        .expect("the file is there");
    assert_eq!(read, written);
}

/// TEST9851: no daemon is not an error.
///
/// A worktree that has never had one, and a machine that has been rebooted,
/// are the same ordinary state: there is no file. The caller's answer is to
/// start a daemon, so this must not be reported as a failure.
#[test]
fn test9851_no_file_is_no_daemon_rather_than_a_failure() {
    let dir = tempfile::tempdir().expect("a scratch directory");
    assert_eq!(
        Rendezvous::read(dir.path()).expect("an absent file is not an error"),
        None
    );
}

/// TEST9852: something else at this path is an error, not "no daemon".
///
/// Starting a second daemon over the top of whatever is writing there would
/// compound it, and reading it as "no daemon" is what would cause that.
#[test]
fn test9852_a_file_that_is_not_a_rendezvous_is_reported() {
    let dir = tempfile::tempdir().expect("a scratch directory");
    std::fs::write(Rendezvous::path_in(dir.path()), b"{not json at all")
        .expect("the fixture is written");

    let error = Rendezvous::read(dir.path())
        .expect_err("a file that is not a rendezvous must be reported");
    assert!(
        error.contains("not a daemon rendezvous file"),
        "the error must say what the file is not: {error}"
    );
}

/// TEST9853: removing it twice is not a failure.
///
/// It is removed on the way out of a process, and more than one path leads
/// there. What the caller wants is for the file to be gone.
#[test]
fn test9853_removing_an_absent_rendezvous_succeeds() {
    let dir = tempfile::tempdir().expect("a scratch directory");
    rendezvous(dir.path())
        .write(dir.path())
        .expect("the file is written");
    Rendezvous::remove(dir.path()).expect("the first removal succeeds");
    Rendezvous::remove(dir.path()).expect("removing an absent file succeeds");
    assert_eq!(Rendezvous::read(dir.path()).expect("readable"), None);
}

/// TEST9854: the rendezvous holds nothing that authorises anything.
///
/// It once carried the key control tokens were signed with, and that made a
/// file on disk the whole authorisation story: anyone who could read it could
/// mint themselves the store. Now the only way in is a session earned with the
/// passphrase, and this asserts the file never grows a shortcut back.
#[test]
fn test9854_the_rendezvous_carries_no_credential() {
    let dir = tempfile::tempdir().expect("a scratch directory");
    rendezvous(dir.path())
        .write(dir.path())
        .expect("the file is written");
    let text = std::fs::read_to_string(Rendezvous::path_in(dir.path()))
        .expect("the file is readable")
        .to_lowercase();

    for forbidden in ["key", "token", "secret", "passphrase", "session"] {
        assert!(
            !text.contains(forbidden),
            "the rendezvous file mentions '{forbidden}':\n{text}\n\
             Nothing in it may authorise anything — a session is earned with \
             the store's passphrase, and a file that grants one makes the \
             passphrase optional."
        );
    }
}

/// TEST9855: it is written where only this user can read it.
///
/// It names a pid and a port on a machine that will be holding credentials,
/// which is not something to publish to every account on a shared host.
#[cfg(unix)]
#[test]
fn test9855_the_rendezvous_is_private() {
    use std::os::unix::fs::PermissionsExt;

    let dir = tempfile::tempdir().expect("a scratch directory");
    rendezvous(dir.path())
        .write(dir.path())
        .expect("the file is written");

    let mode = std::fs::metadata(Rendezvous::path_in(dir.path()))
        .expect("the file is there")
        .permissions()
        .mode()
        & 0o777;
    assert_eq!(mode, 0o600, "the rendezvous file must be readable only by its owner");
}

/// TEST9856: a rewrite replaces the file rather than half-writing it.
///
/// A daemon restarted on a new port writes over the old file. A reader that
/// caught it mid-write would go to a port that belongs to nobody.
#[test]
fn test9856_rewriting_a_rendezvous_replaces_it_whole() {
    let dir = tempfile::tempdir().expect("a scratch directory");
    rendezvous(dir.path())
        .write(dir.path())
        .expect("the first write succeeds");

    let mut second = rendezvous(dir.path());
    second.pid = 5150;
    second.http_port = 3100;
    second.write(dir.path()).expect("the second write succeeds");

    let read = Rendezvous::read(dir.path())
        .expect("readable")
        .expect("present");
    assert_eq!(read, second, "the second write must be what is read back");

    // Nothing left behind: a staged sibling that survived would be read by
    // nothing but would accumulate one file per restart.
    let strays: Vec<String> = std::fs::read_dir(dir.path())
        .expect("the directory is readable")
        .flatten()
        .map(|entry| entry.file_name().to_string_lossy().into_owned())
        .filter(|name| name != "daemon.json")
        .collect();
    assert!(strays.is_empty(), "the write left files behind: {strays:?}");
}