//! Tests for `bxctl`'s command line. Do not edit. use bxctl::cli::{ChatOptions, Command, USAGE, UsageError, parse}; use proto::SessionId; use std::path::{Path, PathBuf}; use std::process::Command as Process; const HOME: &str = "/srv/bx"; fn args(words: &[&str]) -> Vec { words.iter().map(|w| w.to_string()).collect() } fn ok(words: &[&str]) -> Command { parse(&args(words), Path::new(HOME)).unwrap_or_else(|_| panic!("{words:?} must parse")) } fn bad(words: &[&str]) { assert_eq!( parse(&args(words), Path::new(HOME)), Err(UsageError), "{words:?} must be a usage error" ); } fn default_admin() -> PathBuf { PathBuf::from("/srv/bx/run/owner-broker/admin.sock") } #[test] fn chat_defaults_come_from_home() { assert_eq!( ok(&["chat"]), Command::Chat(ChatOptions { socket: PathBuf::from("/srv/bx/run/loop/loop.sock"), admin_socket: default_admin(), session: None, show_thinking: true, say: None, json: false, }) ); } #[test] fn chat_takes_every_flag_in_any_order() { assert_eq!( ok(&[ "chat", "--json", "--admin-socket", "/tmp/a.sock", "--say", "hello there", "--no-thinking", "--session", "s-1", "--socket", "/tmp/l.sock", ]), Command::Chat(ChatOptions { socket: PathBuf::from("/tmp/l.sock"), admin_socket: PathBuf::from("/tmp/a.sock"), session: Some(SessionId::new("s-1").unwrap()), show_thinking: false, say: Some("hello there".to_string()), json: true, }) ); } #[test] fn chat_usage_errors() { bad(&["chat", "--session", "Not Valid!"]); bad(&["chat", "--session"]); bad(&["chat", "--socket"]); bad(&["chat", "--admin-socket"]); bad(&["chat", "--say"]); bad(&["chat", "--dance"]); bad(&["chat", "stray"]); } #[test] fn approvals() { assert_eq!( ok(&["approvals"]), Command::Approvals { admin_socket: default_admin() } ); assert_eq!( ok(&["approvals", "--admin-socket", "/tmp/a.sock"]), Command::Approvals { admin_socket: PathBuf::from("/tmp/a.sock") } ); bad(&["approvals", "41"]); bad(&["approvals", "--admin-socket"]); bad(&["approvals", "--reason", "x"]); } #[test] fn approve() { assert_eq!( ok(&["approve", "41"]), Command::Approve { admin_socket: default_admin(), approval: 41 } ); // The flag may come before or after the id. for words in [ ["approve", "--admin-socket", "/tmp/a.sock", "41"], ["approve", "41", "--admin-socket", "/tmp/a.sock"], ] { assert_eq!( ok(&words), Command::Approve { admin_socket: PathBuf::from("/tmp/a.sock"), approval: 41 } ); } assert_eq!( ok(&["approve", "18446744073709551615"]), Command::Approve { admin_socket: default_admin(), approval: u64::MAX } ); } /// An id is decimal digits and nothing else. `str::parse::` alone would accept `+41`. #[test] fn an_approval_id_is_only_digits() { bad(&["approve"]); bad(&["approve", "41", "42"]); bad(&["approve", "+41"]); bad(&["approve", "-1"]); bad(&["approve", "4 1"]); bad(&["approve", " 41"]); bad(&["approve", "0x29"]); bad(&["approve", "forty-one"]); bad(&["approve", ""]); bad(&["approve", "18446744073709551616"]); bad(&["approve", "41", "--reason", "x"]); bad(&["refuse"]); bad(&["refuse", "+41"]); bad(&["refuse", "41", "42"]); } #[test] fn refuse() { assert_eq!( ok(&["refuse", "41"]), Command::Refuse { admin_socket: default_admin(), approval: 41, reason: None } ); assert_eq!( ok(&[ "refuse", "41", "--reason", "not on a Friday", "--admin-socket", "/tmp/a.sock" ]), Command::Refuse { admin_socket: PathBuf::from("/tmp/a.sock"), approval: 41, reason: Some("not on a Friday".to_string()) } ); // A value is a value, even when it looks like a flag. assert_eq!( ok(&["refuse", "--reason", "--admin-socket", "41"]), Command::Refuse { admin_socket: default_admin(), approval: 41, reason: Some("--admin-socket".to_string()) } ); bad(&["refuse", "41", "--reason"]); bad(&["refuse", "41", "--reason", "a", "--reason", "b"]); } #[test] fn grants_check_and_audit_verify() { assert_eq!( ok(&["grants", "check"]), Command::GrantsCheck { admin_socket: default_admin() } ); assert_eq!( ok(&["grants", "check", "--admin-socket", "/tmp/a.sock"]), Command::GrantsCheck { admin_socket: PathBuf::from("/tmp/a.sock") } ); assert_eq!( ok(&["audit", "verify"]), Command::AuditVerify { home: PathBuf::from(HOME) } ); assert_eq!( ok(&["audit", "verify", "--home", "/tmp/h"]), Command::AuditVerify { home: PathBuf::from("/tmp/h") } ); bad(&["grants"]); bad(&["grants", "list"]); bad(&["grants", "check", "extra"]); bad(&["audit"]); bad(&["audit", "verify", "--home"]); bad(&["audit", "verify", "--admin-socket", "/tmp/a.sock"]); } #[test] fn anything_else_is_a_usage_error() { bad(&[]); bad(&["dance"]); bad(&["--admin-socket", "/tmp/a.sock", "approvals"]); } #[test] fn usage_names_every_command() { for word in [ "bxctl chat", "bxctl approvals", "bxctl approve ", "bxctl refuse ", "bxctl grants check", "bxctl audit verify", "--admin-socket", "--reason", "--home", ] { assert!(USAGE.contains(word), "usage lacks {word:?}"); } } #[test] fn the_binary_prints_usage_and_exits_2() { for words in [ vec![], vec!["dance"], vec!["approve", "+41"], vec!["refuse"], ] { let output = Process::new(env!("CARGO_BIN_EXE_bxctl")) .args(&words) .output() .unwrap(); assert_eq!(output.status.code(), Some(2), "{words:?}"); assert_eq!(String::from_utf8_lossy(&output.stdout), "", "{words:?}"); assert!( String::from_utf8_lossy(&output.stderr).contains("bxctl approve "), "{words:?}" ); } } /// The default sockets are under `$BOXMAKER_HOME`. Nothing listens there, so the command fails /// to connect, and says where it tried. #[test] fn the_binary_finds_the_admin_socket_under_boxmaker_home() { let home = std::env::temp_dir().join(format!("bxctl-cli-home-{}", std::process::id())); let output = Process::new(env!("CARGO_BIN_EXE_bxctl")) .arg("approvals") .env("BOXMAKER_HOME", &home) .output() .unwrap(); assert_eq!(output.status.code(), Some(1)); let stderr = String::from_utf8_lossy(&output.stderr); let want = home.join("run/owner-broker/admin.sock"); assert!(stderr.contains(want.to_str().unwrap()), "{stderr}"); }