Files
saslauthd_yaml/server/tests/shutdown_test.rs
T

131 lines
3.9 KiB
Rust
Raw 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};
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());
}