use common::io::read_string; use common::request::Request; use std::os::unix::fs::FileTypeExt; use std::os::unix::net::UnixStream; use std::path::PathBuf; use std::process::{Child, Command}; use std::time::{Duration, Instant}; const PASSWORD_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../test_passwords"); fn temp_socket_path(tag: &str) -> PathBuf { let mut path = std::env::temp_dir(); path.push(format!("saslauthd-{}-{}.sock", tag, std::process::id())); let _ = std::fs::remove_file(&path); path } /// A spawned daemon that is killed if the test panics before stopping it. struct Daemon(Child); impl Drop for Daemon { fn drop(&mut self) { if self.0.try_wait().ok().flatten().is_none() { let _ = self.0.kill(); let _ = self.0.wait(); } } } fn is_socket(path: &PathBuf) -> bool { std::fs::metadata(path) .map(|m| m.file_type().is_socket()) .unwrap_or(false) } fn spawn_daemon(socket: &PathBuf) -> Daemon { let child = Command::new(env!("CARGO_BIN_EXE_saslauthd")) .arg("--socket-name") .arg(socket) .arg("--password-dir") .arg(PASSWORD_DIR) .spawn() .expect("failed to spawn saslauthd"); let deadline = Instant::now() + Duration::from_secs(5); while !is_socket(socket) { assert!(Instant::now() < deadline, "daemon never created {socket:?}"); std::thread::sleep(Duration::from_millis(10)); } Daemon(child) } fn send_signal(daemon: &Daemon, signal: &str) { let child = &daemon.0; let status = Command::new("kill") .arg(format!("-{signal}")) .arg(child.id().to_string()) .status() .expect("failed to run kill"); assert!(status.success(), "kill -{signal} failed"); } fn wait_for_exit(daemon: &mut Daemon, timeout: Duration) -> std::process::ExitStatus { let child = &mut daemon.0; let deadline = Instant::now() + timeout; loop { if let Some(status) = child.try_wait().expect("try_wait failed") { return status; } if Instant::now() >= deadline { let _ = child.kill(); let _ = child.wait(); panic!("daemon did not exit within {timeout:?}"); } std::thread::sleep(Duration::from_millis(10)); } } fn authenticate(socket: &PathBuf, password: &str) -> String { let mut stream = UnixStream::connect(socket).expect("connect to daemon"); Request::new("bp", password, "imap", "") .write_to_stream(&mut stream) .expect("write request"); read_string(&mut stream).expect("read response") } fn assert_clean_shutdown(signal: &str) { let socket = temp_socket_path(signal); let mut daemon = spawn_daemon(&socket); // Make sure the daemon actually works before we shut it down. assert_eq!(authenticate(&socket, "some-rad-password"), "OK"); assert_eq!(authenticate(&socket, "wrong-password"), "NO"); let started = Instant::now(); send_signal(&daemon, signal); let status = wait_for_exit(&mut daemon, Duration::from_secs(1)); assert!(status.success(), "expected exit status 0 on {signal}, got {status}"); assert!( started.elapsed() < Duration::from_secs(1), "shutdown took {:?}", started.elapsed() ); assert!(!socket.exists(), "socket file {socket:?} was left behind"); } #[test] fn it_shuts_down_cleanly_on_sigterm() { assert_clean_shutdown("TERM"); } #[test] fn it_shuts_down_cleanly_on_sigint() { assert_clean_shutdown("INT"); } #[test] fn it_recreates_a_stale_socket_on_startup() { let socket = temp_socket_path("stale"); std::fs::write(&socket, b"stale").expect("create stale socket file"); let mut daemon = spawn_daemon(&socket); assert_eq!(authenticate(&socket, "some-rad-password"), "OK"); send_signal(&daemon, "TERM"); let status = wait_for_exit(&mut daemon, Duration::from_secs(1)); assert!(status.success()); assert!(!socket.exists()); }