wsb-daemon 0.258.6441

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
//! What the daemon was started with, and what it refuses to start on.
//!
//! Every argument is required and every malformed one is a refusal, because a
//! daemon is started by a program rather than typed by a person: a lenient
//! parser here would let a defect in the launcher become a daemon serving the
//! wrong workspace on a port nobody expected.

use super::*;

use std::net::{IpAddr, Ipv4Addr};

fn args(items: &[&str]) -> Vec<String> {
    items.iter().map(|s| (*s).to_string()).collect()
}

fn complete(workspace: &std::path::Path) -> Vec<String> {
    args(&[
        "--workspace",
        &workspace.display().to_string(),
        "--port",
        "3000",
    ])
}

/// TEST9862: the arguments a launcher passes produce the configuration it meant.
#[test]
fn test9862_a_complete_command_line_is_the_configuration() {
    let dir = tempfile::tempdir().expect("a scratch directory");
    let config = Config::from_args(complete(dir.path())).expect("a complete command line parses");

    // A workspace on the command line is one to register at startup, not the
    // daemon's own: one daemon serves the machine, and registers the rest as
    // commands are run in them.
    assert_eq!(config.workspace_root.as_deref(), Some(dir.path()));
    assert_eq!(config.http_port, 3000);
    assert!(!config.debug, "debug is off unless it is asked for");
    assert_eq!(config.parent_pid, None, "an ordinary daemon dies with nobody");
    assert_eq!(
        config.listen,
        IpAddr::V4(Ipv4Addr::LOCALHOST),
        "a daemon nobody told otherwise is reachable only from this machine"
    );
}

/// TEST9897: reaching beyond this machine takes saying so.
///
/// The workbench has no authentication: it reads the workspace and starts
/// operations on this host. Which address it binds is therefore a decision somebody makes out loud, per
/// start — never a default, and never a stored preference that could bring the
/// port back up on a restart nobody was watching.
#[test]
fn test9897_the_listen_address_is_loopback_unless_it_is_asked_for() {
    let dir = tempfile::tempdir().expect("a scratch directory");

    let default = Config::from_args(complete(dir.path())).expect("parses");
    assert!(
        default.listen.is_loopback(),
        "the default must not be reachable from another machine"
    );

    let mut argv = complete(dir.path());
    argv.extend(args(&["--listen", "0.0.0.0"]));
    let everywhere = Config::from_args(argv).expect("an explicit address parses");
    assert_eq!(everywhere.listen, IpAddr::V4(Ipv4Addr::UNSPECIFIED));
    assert!(!everywhere.listen.is_loopback());

    let mut argv = complete(dir.path());
    argv.extend(args(&["--listen", "192.168.1.10"]));
    let one_interface = Config::from_args(argv).expect("a specific address parses");
    assert_eq!(one_interface.listen, "192.168.1.10".parse::<IpAddr>().unwrap());
}

/// TEST9898: an address that is not one is refused.
///
/// A typo here is the difference between "my machine" and "every machine that
/// can route to mine", so it stops the daemon rather than being interpreted.
#[test]
fn test9898_a_listen_address_that_is_not_an_address_is_refused() {
    let dir = tempfile::tempdir().expect("a scratch directory");
    let mut argv = complete(dir.path());
    argv.extend(args(&["--listen", "everywhere"]));

    let error = Config::from_args(argv).expect_err("that is not an address");
    assert!(
        error.contains("--listen must be an IP address"),
        "the refusal must say what was wanted: {error}"
    );
}

/// TEST9863: the optional flags are read when they are given.
#[test]
fn test9863_the_optional_flags_are_read() {
    let dir = tempfile::tempdir().expect("a scratch directory");
    let mut argv = complete(dir.path());
    argv.extend(args(&["--debug", "--parent-pid", "4242"]));

    let config = Config::from_args(argv).expect("the optional flags parse");
    assert!(config.debug);
    assert_eq!(config.parent_pid, Some(4242));
}

/// TEST9864: each required argument is required.
///
/// One at a time, so a missing `--port` cannot be masked by a missing
/// `--workspace` being reported first.
#[test]
fn test9864_every_required_argument_is_required() {
    let dir = tempfile::tempdir().expect("a scratch directory");
    let workspace = dir.path().display().to_string();

    let cases: [(&str, Vec<String>); 1] = [("--port", args(&["--workspace", &workspace]))];

    for (missing, argv) in cases {
        let error = Config::from_args(argv)
            .unwrap_err_or_else(|| format!("{missing} was not required"));
        assert!(
            error.contains(missing),
            "the refusal must name what is missing ({missing}): {error}"
        );
    }

    // `--workspace` is NOT among them any more. One daemon serves the machine,
    // and it registers workspaces as commands are run in them — a daemon
    // started with none is a machine nobody has opened a workspace on yet,
    // which is an ordinary state rather than a missing argument.
    let without = Config::from_args(args(&["--port", "3000"]))
        .expect("a daemon may start without a workspace");
    assert_eq!(without.workspace_root, None);
}

/// TEST9997: the two listeners have opposite defaults, on purpose.
///
/// They protect different things, so a single default would be wrong for one
/// of them:
///
/// * The workbench is unauthenticated HTTP — it reads the workspace and starts
///   operations on this host. Anyone who reaches it has this machine, so it
///   stays on loopback.
/// * The peer port answers only machines whose identity this one has been
///   paired with by hand, over an encrypted session. Bound to loopback it can
///   be reached by no peer at all, which makes continuity impossible without a
///   tunnel — a default that quietly disables the feature it belongs to.
#[test]
fn test9997_the_workbench_stays_local_and_peers_can_reach_the_machine() {
    let dir = tempfile::tempdir().expect("a scratch directory");
    let config = Config::from_args(complete(dir.path())).expect("parses");

    assert!(
        config.listen.is_loopback(),
        "the workbench must not be reachable from another machine by default"
    );
    assert_eq!(
        config.peer_listen,
        IpAddr::V4(Ipv4Addr::UNSPECIFIED),
        "peers must be able to reach this machine without anybody configuring it"
    );

    // Both are still decisions somebody can make out loud, in either
    // direction: a machine that peers only over one interface says so, and a
    // workbench somebody deliberately exposes says so too.
    let narrowed = Config::from_args({
        let mut argv = complete(dir.path());
        argv.extend(args(&["--peer-listen", "192.168.1.10"]));
        argv
    })
    .expect("parses");
    assert_eq!(narrowed.peer_listen.to_string(), "192.168.1.10");
    assert!(narrowed.listen.is_loopback(), "narrowing one must not move the other");

    let exposed = Config::from_args({
        let mut argv = complete(dir.path());
        argv.extend(args(&["--listen", "0.0.0.0"]));
        argv
    })
    .expect("parses");
    assert_eq!(exposed.listen, IpAddr::V4(Ipv4Addr::UNSPECIFIED));
    assert_eq!(
        exposed.peer_listen,
        IpAddr::V4(Ipv4Addr::UNSPECIFIED),
        "exposing the workbench says nothing about the peer listener"
    );

    // And an address that is not one is refused rather than guessed at.
    let error = Config::from_args({
        let mut argv = complete(dir.path());
        argv.extend(args(&["--peer-listen", "everywhere"]));
        argv
    })
    .unwrap_err_or_else(|| "a non-address was accepted".to_string());
    assert!(error.contains("--peer-listen"), "{error}");
}

/// TEST9865: a flag with no value is a refusal, not a flag with an empty one.
#[test]
fn test9865_a_flag_without_its_value_is_refused() {
    let error = Config::from_args(args(&["--workspace"]))
        .expect_err("a flag with no value must be refused");
    assert!(
        error.contains("--workspace requires a value"),
        "the refusal must name the flag: {error}"
    );
}

/// TEST9866: the workspace must be an absolute directory that is there.
///
/// The daemon changes into it and resolves the state directory from it. A
/// relative path would resolve against whatever directory the launcher
/// happened to be in, which is how a daemon ends up registering the wrong
/// workspace.
#[test]
fn test9866_the_workspace_must_be_an_absolute_directory() {
    let relative = Config::from_args(args(&[
        "--workspace",
        "wsb",
        "--port",
        "3000",
    ]))
    .expect_err("a relative workspace must be refused");
    assert!(
        relative.contains("must be absolute"),
        "the refusal must say why: {relative}"
    );

    let dir = tempfile::tempdir().expect("a scratch directory");
    let file = dir.path().join("not-a-directory");
    std::fs::write(&file, b"").expect("the fixture is written");
    let not_a_dir = Config::from_args(args(&[
        "--workspace",
        &file.display().to_string(),
        "--port",
        "3000",
    ]))
    .expect_err("a workspace that is not a directory must be refused");
    assert!(
        not_a_dir.contains("not a directory"),
        "the refusal must say why: {not_a_dir}"
    );
}

/// TEST9867: a port of zero is refused.
///
/// Zero means "any free port" to the operating system, and a workbench on a
/// port nobody can predict is one the rendezvous file would have to be right
/// about before it could be written.
#[test]
fn test9867_a_port_of_zero_is_refused() {
    let dir = tempfile::tempdir().expect("a scratch directory");
    let error = Config::from_args(args(&[
        "--workspace",
        &dir.path().display().to_string(),
        "--port",
        "0",
    ]))
    .expect_err("port 0 must be refused");
    assert!(error.contains("must not be 0"), "{error}");
}

/// TEST9869: an argument nobody recognises stops the daemon.
///
/// The launcher and the daemon are two halves of one program. A flag one side
/// passes and the other ignores is a setting the operator asked for and did
/// not get.
#[test]
fn test9869_an_unknown_argument_is_refused() {
    let dir = tempfile::tempdir().expect("a scratch directory");
    let mut argv = complete(dir.path());
    argv.push("--serve-everything".to_string());

    let error = Config::from_args(argv).expect_err("an unknown flag must be refused");
    assert!(
        error.contains("--serve-everything"),
        "the refusal must name it: {error}"
    );
}

/// TEST9870: a parent pid of zero is refused.
///
/// Zero is not a process this daemon can watch; on unix it names the caller's
/// own process group, and a liveness probe against it would say "alive" for
/// ever — a daemon that promised to die with its parent and never would.
#[test]
fn test9870_a_parent_pid_of_zero_is_refused() {
    let dir = tempfile::tempdir().expect("a scratch directory");
    let mut argv = complete(dir.path());
    argv.extend(args(&["--parent-pid", "0"]));

    let error = Config::from_args(argv).expect_err("pid 0 must be refused");
    assert!(error.contains("must not be 0"), "{error}");
}

/// A refusal, or a message saying what should have been refused.
trait UnwrapErrOrElse<T> {
    fn unwrap_err_or_else(self, message: impl FnOnce() -> String) -> String;
}

impl<T: std::fmt::Debug> UnwrapErrOrElse<T> for Result<T, String> {
    fn unwrap_err_or_else(self, message: impl FnOnce() -> String) -> String {
        match self {
            Ok(value) => panic!("{}: parsed as {value:?}", message()),
            Err(error) => error,
        }
    }
}