Files

144 lines
4.5 KiB
Rust
Raw Permalink Normal View History

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};
/// Idle shutdown takes milliseconds; anything approaching the daemon's 3s
/// grace period means it is waiting on something it should not be.
const EXIT_TIMEOUT: Duration = Duration::from_secs(2);
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");
// Wrap immediately so a failed startup below still kills the process.
let daemon = Daemon(child);
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
}
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 {
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");
send_signal(&daemon, signal);
let status = wait_for_exit(&mut daemon, EXIT_TIMEOUT);
assert!(status.success(), "expected exit status 0 on {signal}, got {status}");
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, EXIT_TIMEOUT);
assert!(status.success());
assert!(!socket.exists());
}
#[test]
fn it_does_not_wait_forever_for_a_silent_client() {
let socket = temp_socket_path("silent");
let mut daemon = spawn_daemon(&socket);
// Connect but never send a request, leaving a handler blocked on read.
let _silent = UnixStream::connect(&socket).expect("connect to daemon");
send_signal(&daemon, "TERM");
// Must finish once the grace period (3s) expires, whatever the client does.
let status = wait_for_exit(&mut daemon, Duration::from_secs(5));
assert!(status.success());
assert!(!socket.exists());
}