//! The dispatcher's contract, held against stand-in verb binaries on a //! `PATH` of the test's making: what a verb receives, what it does not //! receive, and what a person is told when nothing answers. #![cfg(unix)] use std::path::{Path, PathBuf}; use std::process::{Command, Output}; /// A directory on `PATH` holding shell scripts that stand in for verb /// binaries. Each prints its arguments and every `DIDBOT_` variable it /// sees, and answers `--version` with a version of its own. struct Fixture { dir: PathBuf, } impl Fixture { fn new(name: &str) -> Self { let dir = std::env::temp_dir().join(format!("didbot-dispatch-{name}-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).expect("a fixture directory"); Self { dir } } fn script(&self, binary: &str, version: &str) -> &Self { use std::os::unix::fs::PermissionsExt; let body = format!( "#!/bin/sh\n\ if [ \"$1\" = \"--version\" ]; then echo \"{binary} {version}\"; exit 0; fi\n\ echo \"argv: $*\"\n\ env | grep '^DIDBOT_' | sort\n" ); let path = self.dir.join(binary); std::fs::write(&path, body).expect("the script writes"); std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).expect("chmod"); self } /// One that refuses `--version` the way a binary with no such flag /// would. fn mute(&self, binary: &str) -> &Self { use std::os::unix::fs::PermissionsExt; let path = self.dir.join(binary); std::fs::write(&path, "#!/bin/sh\necho \"no such flag: $1\" >&2\nexit 2\n") .expect("the script writes"); std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).expect("chmod"); self } fn didbot(&self, args: &[&str]) -> Output { Command::new(env!("CARGO_BIN_EXE_didbot")) .args(args) .env("PATH", format!("{}:/usr/bin:/bin", self.dir.display())) .env("DIDBOT_ACCOUNT_TOKEN", "a-secret") .env("DIDBOT_ACCOUNT_TOKEN_FILE", "/run/secrets/token") .env("DIDBOT_PDS", "pds.example") .output() .expect("run didbot") } } impl Drop for Fixture { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.dir); } } fn stdout(output: &Output) -> String { String::from_utf8_lossy(&output.stdout).into_owned() } fn stderr(output: &Output) -> String { String::from_utf8_lossy(&output.stderr).into_owned() } /// The whole reason the dispatcher exists as a separate process: a verb /// gets the command line and its caller's environment minus every /// credential variable, and nothing is added. #[test] fn a_verb_receives_argv_and_no_credential() { let fixture = Fixture::new("scrub"); fixture.script("didbot-operator", "1.0.0"); let output = fixture.didbot(&["estop", "--server", "pds.example", "--pause"]); assert!(output.status.success(), "{}", stderr(&output)); let seen = stdout(&output); assert!( seen.starts_with("argv: estop --server pds.example --pause\n"), "the verb did not get the command line as typed:\n{seen}" ); assert!( !seen.contains("DIDBOT_ACCOUNT_TOKEN"), "a credential variable reached the verb:\n{seen}" ); assert!( seen.contains("DIDBOT_PDS=pds.example"), "a variable that carries no credential was scrubbed too:\n{seen}" ); } /// `didbot-oauth` has subcommands of its own, so `oauth` is dropped rather /// than forwarded. #[test] fn oauth_forwards_what_follows_the_verb() { let fixture = Fixture::new("oauth"); fixture.script("didbot-oauth", "1.0.0"); let output = fixture.didbot(&["oauth", "approve", "tok-1"]); assert!(output.status.success(), "{}", stderr(&output)); assert!( stdout(&output).starts_with("argv: approve tok-1\n"), "{}", stdout(&output) ); } /// `register` is one verb of its own binary, so it is dropped the same way. #[test] fn register_forwards_what_follows_the_verb() { let fixture = Fixture::new("register"); fixture.script("didbot-register", "1.0.0"); let output = fixture.didbot(&["register", "host", "h1.pds.example", "--timeout", "60"]); assert!(output.status.success(), "{}", stderr(&output)); assert!( stdout(&output).starts_with("argv: host h1.pds.example --timeout 60\n"), "{}", stdout(&output) ); } /// An unknown word is `didbot-` on `PATH`, scrubbed the same way. #[test] fn another_word_runs_the_binary_of_that_name() { let fixture = Fixture::new("external"); fixture.script("didbot-echo", "0.0.1"); let output = fixture.didbot(&["echo", "one", "--two"]); assert!(output.status.success(), "{}", stderr(&output)); let seen = stdout(&output); assert!(seen.starts_with("argv: one --two\n"), "{seen}"); assert!( !seen.contains("DIDBOT_ACCOUNT_TOKEN"), "a credential variable reached an external verb:\n{seen}" ); } /// The first-party verbs are known even when their code is absent: the /// answer names the binary to install, and is a failure rather than a usage /// error because the command line was fine. #[test] fn a_first_party_verb_without_its_binary_says_what_to_install() { let fixture = Fixture::new("missing"); for verb in ["operate", "login", "estop", "announce"] { let output = fixture.didbot(&[verb, "--server", "pds.example"]); assert_eq!(output.status.code(), Some(1), "{verb}: {}", stderr(&output)); assert!( stderr(&output).contains("install didbot-operator"), "{verb}: {}", stderr(&output) ); } // Two binaries from one crate: the answer names what to install. for args in [ &["oauth", "pending"][..], &["register", "host", "h1.pds.example"], ] { let output = fixture.didbot(args); assert_eq!(output.status.code(), Some(1), "{args:?}"); assert!( stderr(&output).contains("install didbot-agentd"), "{args:?}: {}", stderr(&output) ); } } #[test] fn an_unknown_word_with_no_binary_is_a_usage_error() { let fixture = Fixture::new("unknown"); let output = fixture.didbot(&["frobnicate"]); assert_eq!(output.status.code(), Some(2), "{}", stderr(&output)); let said = stderr(&output); assert!(said.contains("unknown command `frobnicate`"), "{said}"); assert!(said.contains("didbot --list"), "{said}"); } /// One line per binary — name, version, path — and the version is whatever /// the binary itself says, so two binaries at different versions show it. #[test] fn list_shows_name_version_and_path() { let fixture = Fixture::new("list"); fixture .script("didbot-operator", "7.7.7") .script("didbot-oauth", "5.5.5") .script("didbot-register", "5.5.5") .mute("didbot-swarm"); let output = fixture.didbot(&["--list"]); assert!(output.status.success(), "{}", stderr(&output)); let listed = stdout(&output); let rows: Vec> = listed .lines() .map(|line| line.split_whitespace().collect()) .collect(); assert_eq!( rows[0][..2], ["didbot", env!("CARGO_PKG_VERSION")], "the dispatcher lists itself first:\n{listed}" ); let row = |name: &str| { rows.iter() .find(|row| row[0] == name) .unwrap_or_else(|| panic!("{name} is not listed:\n{listed}")) .clone() }; let in_fixture = |path: &str| Path::new(path).starts_with(&fixture.dir); assert_eq!(row("didbot-operator")[1], "7.7.7"); assert!(in_fixture(row("didbot-operator")[2])); assert_eq!(row("didbot-oauth")[1], "5.5.5"); assert_eq!( row("didbot-swarm")[1], "?", "a binary that does not answer --version is listed, not dropped:\n{listed}" ); assert!( !listed.contains("not installed"), "every first-party binary is present:\n{listed}" ); } #[test] fn list_names_the_first_party_binaries_that_are_absent() { let fixture = Fixture::new("list-absent"); let output = fixture.didbot(&["--list"]); assert!(output.status.success(), "{}", stderr(&output)); let listed = stdout(&output); assert!( listed.contains( "didbot-operator: not installed (operate, login, estop, account, app, announce)" ), "{listed}" ); assert!( listed.contains("didbot-oauth: not installed (oauth)"), "{listed}" ); assert!( listed.contains("didbot-register: not installed (register)"), "{listed}" ); } /// `didbot help ` reaches the verb's own `--help`, through the same /// table. #[test] fn help_verb_asks_the_binary() { let fixture = Fixture::new("help"); fixture .script("didbot-operator", "1.0.0") .script("didbot-echo", "1.0.0"); let output = fixture.didbot(&["help", "estop"]); assert!( stdout(&output).starts_with("argv: estop --help\n"), "{}", stdout(&output) ); let output = fixture.didbot(&["help", "echo"]); assert!( stdout(&output).starts_with("argv: --help\n"), "{}", stdout(&output) ); } #[test] fn help_and_version_answer_on_stdout() { let fixture = Fixture::new("own-help"); let output = fixture.didbot(&["--help"]); assert!(output.status.success()); let help = stdout(&output); for verb in [ "operate", "login", "estop", "announce", "oauth", "register", "--list", ] { assert!( help.contains(verb), "--help does not mention {verb}:\n{help}" ); } assert!(stderr(&output).is_empty()); let output = fixture.didbot(&["--version"]); assert!(output.status.success()); assert_eq!( stdout(&output).trim(), format!("didbot {}", env!("CARGO_PKG_VERSION")) ); let output = fixture.didbot(&[]); assert_eq!( output.status.code(), Some(2), "an empty command line is a usage error" ); }